summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-09-19 23:18:09 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2022-09-19 23:18:09 +0000
commit6ed4ec3e0b1340f96b7c043ef51d1b33bbe85fde (patch)
treedc4d20fe6064752c0bd323187252c77e0a89144b /lib
parent9868dae7fc0655bd7ce4a6887d4e6d487690eeed (diff)
downloadgitlab-ce-6ed4ec3e0b1340f96b7c043ef51d1b33bbe85fde.tar.gz
Add latest changes from gitlab-org/gitlab@15-4-stable-eev15.4.0-rc42
Diffstat (limited to 'lib')
-rw-r--r--lib/api/admin/batched_background_migrations.rb108
-rw-r--r--lib/api/api.rb5
-rw-r--r--lib/api/branches.rb37
-rw-r--r--lib/api/ci/job_artifacts.rb2
-rw-r--r--lib/api/ci/jobs.rb7
-rw-r--r--lib/api/ci/runners.rb15
-rw-r--r--lib/api/composer_packages.rb5
-rw-r--r--lib/api/concerns/packages/conan_endpoints.rb8
-rw-r--r--lib/api/concerns/packages/debian_package_endpoints.rb123
-rw-r--r--lib/api/debian_group_packages.rb2
-rw-r--r--lib/api/debian_project_packages.rb2
-rw-r--r--lib/api/entities/batched_background_migration.rb14
-rw-r--r--lib/api/entities/ci/job_basic.rb6
-rw-r--r--lib/api/entities/ci/job_request/image.rb2
-rw-r--r--lib/api/entities/ci/job_request/service.rb2
-rw-r--r--lib/api/entities/merge_request_reviewer.rb1
-rw-r--r--lib/api/entities/ml/mlflow/experiment.rb28
-rw-r--r--lib/api/entities/ml/mlflow/new_experiment.rb19
-rw-r--r--lib/api/entities/ml/mlflow/run.rb16
-rw-r--r--lib/api/entities/ml/mlflow/run_info.rb27
-rw-r--r--lib/api/entities/ml/mlflow/update_run.rb19
-rw-r--r--lib/api/entities/package.rb1
-rw-r--r--lib/api/entities/personal_access_token_with_details.rb13
-rw-r--r--lib/api/entities/user_safe.rb6
-rw-r--r--lib/api/generic_packages.rb2
-rw-r--r--lib/api/groups.rb21
-rw-r--r--lib/api/helm_packages.rb2
-rw-r--r--lib/api/helpers.rb29
-rw-r--r--lib/api/helpers/groups_helpers.rb3
-rw-r--r--lib/api/helpers/packages/conan/api_helpers.rb6
-rw-r--r--lib/api/helpers/packages/dependency_proxy_helpers.rb20
-rw-r--r--lib/api/helpers/packages_helpers.rb7
-rw-r--r--lib/api/helpers/personal_access_tokens_helpers.rb35
-rw-r--r--lib/api/helpers/projects_helpers.rb5
-rw-r--r--lib/api/helpers/resource_events_helpers.rb17
-rw-r--r--lib/api/helpers/resource_label_events_helpers.rb18
-rw-r--r--lib/api/integrations/slack/events.rb40
-rw-r--r--lib/api/integrations/slack/events/url_verification.rb22
-rw-r--r--lib/api/integrations/slack/request.rb51
-rw-r--r--lib/api/internal/base.rb12
-rw-r--r--lib/api/maven_packages.rb79
-rw-r--r--lib/api/members.rb1
-rw-r--r--lib/api/merge_requests.rb25
-rw-r--r--lib/api/ml/mlflow.rb171
-rw-r--r--lib/api/namespaces.rb2
-rw-r--r--lib/api/npm_project_packages.rb2
-rw-r--r--lib/api/nuget_project_packages.rb2
-rw-r--r--lib/api/personal_access_tokens.rb34
-rw-r--r--lib/api/personal_access_tokens/self_revocation.rb26
-rw-r--r--lib/api/projects.rb18
-rw-r--r--lib/api/pypi_packages.rb4
-rw-r--r--lib/api/releases.rb66
-rw-r--r--lib/api/resource_label_events.rb12
-rw-r--r--lib/api/resource_state_events.rb30
-rw-r--r--lib/api/rpm_project_packages.rb63
-rw-r--r--lib/api/rubygem_packages.rb4
-rw-r--r--lib/api/search.rb17
-rw-r--r--lib/api/settings.rb1
-rw-r--r--lib/api/tags.rb4
-rw-r--r--lib/api/topics.rb20
-rw-r--r--lib/api/users.rb5
-rw-r--r--lib/banzai/filter/blockquote_fence_filter.rb6
-rw-r--r--lib/banzai/filter/kroki_filter.rb11
-rw-r--r--lib/banzai/filter/math_filter.rb100
-rw-r--r--lib/banzai/filter/plantuml_filter.rb3
-rw-r--r--lib/banzai/filter/references/reference_filter.rb12
-rw-r--r--lib/banzai/pipeline/markup_pipeline.rb4
-rw-r--r--lib/bulk_imports/file_downloads/filename_fetch.rb46
-rw-r--r--lib/bulk_imports/file_downloads/validations.rb58
-rw-r--r--lib/container_registry/gitlab_api_client.rb2
-rw-r--r--lib/container_registry/tag.rb13
-rw-r--r--lib/error_tracking/sentry_client.rb20
-rw-r--r--lib/error_tracking/sentry_client/issue.rb12
-rw-r--r--lib/event_filter.rb25
-rw-r--r--lib/generators/gitlab/usage_metric/templates/instrumentation_class_spec.rb.template4
-rw-r--r--lib/gitlab/abuse.rb6
-rw-r--r--lib/gitlab/access.rb20
-rw-r--r--lib/gitlab/alert_management/payload/base.rb4
-rw-r--r--lib/gitlab/alert_management/payload/generic.rb9
-rw-r--r--lib/gitlab/analytics/cycle_analytics/request_params.rb5
-rw-r--r--lib/gitlab/analytics/date_filler.rb112
-rw-r--r--lib/gitlab/application_rate_limiter.rb79
-rw-r--r--lib/gitlab/application_rate_limiter/increment_per_action.rb6
-rw-r--r--lib/gitlab/application_rate_limiter/increment_per_actioned_resource.rb8
-rw-r--r--lib/gitlab/audit/auditor.rb2
-rw-r--r--lib/gitlab/audit/type/definition.rb122
-rw-r--r--lib/gitlab/auth/ldap/config.rb14
-rw-r--r--lib/gitlab/auth/o_auth/auth_hash.rb4
-rw-r--r--lib/gitlab/auth/o_auth/provider.rb18
-rw-r--r--lib/gitlab/auth/o_auth/user.rb10
-rw-r--r--lib/gitlab/auth/otp/strategies/forti_authenticator/manual_otp.rb2
-rw-r--r--lib/gitlab/auth/user_access_denied_reason.rb2
-rw-r--r--lib/gitlab/background_migration/backfill_cluster_agents_has_vulnerabilities.rb31
-rw-r--r--lib/gitlab/background_migration/backfill_draft_status_on_merge_requests_with_corrected_regex.rb1
-rw-r--r--lib/gitlab/background_migration/backfill_project_feature_package_registry_access_level.rb2
-rw-r--r--lib/gitlab/background_migration/backfill_project_namespace_on_issues.rb25
-rw-r--r--lib/gitlab/background_migration/backfill_project_repositories.rb4
-rw-r--r--lib/gitlab/background_migration/backfill_vulnerability_reads_cluster_agent.rb12
-rw-r--r--lib/gitlab/background_migration/backfill_work_item_type_id_for_issues.rb32
-rw-r--r--lib/gitlab/background_migration/batching_strategies/backfill_issue_work_item_type_batching_strategy.rb19
-rw-r--r--lib/gitlab/background_migration/batching_strategies/backfill_project_statistics_with_container_registry_size_batching_strategy.rb13
-rw-r--r--lib/gitlab/background_migration/batching_strategies/backfill_vulnerability_reads_cluster_agent_batching_strategy.rb11
-rw-r--r--lib/gitlab/background_migration/batching_strategies/dismissed_vulnerabilities_strategy.rb9
-rw-r--r--lib/gitlab/background_migration/batching_strategies/primary_key_batching_strategy.rb16
-rw-r--r--lib/gitlab/background_migration/batching_strategies/remove_backfilled_job_artifacts_expire_at_batching_strategy.rb12
-rw-r--r--lib/gitlab/background_migration/delete_approval_rules_with_vulnerability.rb16
-rw-r--r--lib/gitlab/background_migration/destroy_invalid_group_members.rb23
-rw-r--r--lib/gitlab/background_migration/destroy_invalid_project_members.rb25
-rw-r--r--lib/gitlab/background_migration/disable_legacy_open_source_licence_for_recent_public_projects.rb29
-rw-r--r--lib/gitlab/background_migration/disable_legacy_open_source_license_for_projects_less_than_one_mb.rb21
-rw-r--r--lib/gitlab/background_migration/mailers/unconfirm_mailer.rb2
-rw-r--r--lib/gitlab/background_migration/recalculate_vulnerabilities_occurrences_uuid.rb2
-rw-r--r--lib/gitlab/background_migration/remove_backfilled_job_artifacts_expire_at.rb43
-rw-r--r--lib/gitlab/background_migration/remove_self_managed_wiki_notes.rb16
-rw-r--r--lib/gitlab/background_migration/rename_task_system_note_to_checklist_item.rb28
-rw-r--r--lib/gitlab/background_migration/set_correct_vulnerability_state.rb7
-rw-r--r--lib/gitlab/base_doorkeeper_controller.rb2
-rw-r--r--lib/gitlab/cache/helpers.rb75
-rw-r--r--lib/gitlab/ci/ansi2html.rb10
-rw-r--r--lib/gitlab/ci/ansi2json/parser.rb10
-rw-r--r--lib/gitlab/ci/build/artifacts/adapters/zip_stream.rb61
-rw-r--r--lib/gitlab/ci/build/context/build.rb13
-rw-r--r--lib/gitlab/ci/build/rules/rule/clause/exists.rb2
-rw-r--r--lib/gitlab/ci/config/entry/current_variables.rb49
-rw-r--r--lib/gitlab/ci/config/entry/environment.rb2
-rw-r--r--lib/gitlab/ci/config/entry/image.rb7
-rw-r--r--lib/gitlab/ci/config/entry/imageable.rb5
-rw-r--r--lib/gitlab/ci/config/entry/legacy_variables.rb46
-rw-r--r--lib/gitlab/ci/config/entry/processable.rb4
-rw-r--r--lib/gitlab/ci/config/entry/root.rb3
-rw-r--r--lib/gitlab/ci/config/entry/service.rb9
-rw-r--r--lib/gitlab/ci/config/entry/variable.rb98
-rw-r--r--lib/gitlab/ci/config/entry/variables.rb40
-rw-r--r--lib/gitlab/ci/jwt_v2.rb2
-rw-r--r--lib/gitlab/ci/parsers/sbom/cyclonedx.rb14
-rw-r--r--lib/gitlab/ci/parsers/sbom/source/dependency_scanning.rb10
-rw-r--r--lib/gitlab/ci/parsers/security/common.rb24
-rw-r--r--lib/gitlab/ci/parsers/security/validators/schema_validator.rb16
-rw-r--r--lib/gitlab/ci/parsers/security/validators/schemas/14.1.3/cluster-image-scanning-report-format.json977
-rw-r--r--lib/gitlab/ci/parsers/security/validators/schemas/14.1.3/container-scanning-report-format.json911
-rw-r--r--lib/gitlab/ci/parsers/security/validators/schemas/14.1.3/coverage-fuzzing-report-format.json874
-rw-r--r--lib/gitlab/ci/parsers/security/validators/schemas/14.1.3/dast-report-format.json1287
-rw-r--r--lib/gitlab/ci/parsers/security/validators/schemas/14.1.3/dependency-scanning-report-format.json968
-rw-r--r--lib/gitlab/ci/parsers/security/validators/schemas/14.1.3/sast-report-format.json869
-rw-r--r--lib/gitlab/ci/parsers/security/validators/schemas/14.1.3/secret-detection-report-format.json892
-rw-r--r--lib/gitlab/ci/parsers/security/validators/schemas/15.0.0/cluster-image-scanning-report-format.json946
-rw-r--r--lib/gitlab/ci/parsers/security/validators/schemas/15.0.0/container-scanning-report-format.json880
-rw-r--r--lib/gitlab/ci/parsers/security/validators/schemas/15.0.0/coverage-fuzzing-report-format.json836
-rw-r--r--lib/gitlab/ci/parsers/security/validators/schemas/15.0.0/dast-report-format.json1241
-rw-r--r--lib/gitlab/ci/parsers/security/validators/schemas/15.0.0/dependency-scanning-report-format.json944
-rw-r--r--lib/gitlab/ci/parsers/security/validators/schemas/15.0.0/sast-report-format.json831
-rw-r--r--lib/gitlab/ci/parsers/security/validators/schemas/15.0.0/secret-detection-report-format.json854
-rw-r--r--lib/gitlab/ci/parsers/test/junit.rb4
-rw-r--r--lib/gitlab/ci/pipeline/chain/assign_partition.rb31
-rw-r--r--lib/gitlab/ci/pipeline/chain/command.rb12
-rw-r--r--lib/gitlab/ci/pipeline/chain/config/content.rb23
-rw-r--r--lib/gitlab/ci/pipeline/chain/config/content/source.rb1
-rw-r--r--lib/gitlab/ci/pipeline/chain/ensure_environments.rb4
-rw-r--r--lib/gitlab/ci/pipeline/chain/validate/external.rb9
-rw-r--r--lib/gitlab/ci/pipeline/seed/build.rb4
-rw-r--r--lib/gitlab/ci/pipeline/seed/environment.rb16
-rw-r--r--lib/gitlab/ci/pipeline/seed/stage.rb3
-rw-r--r--lib/gitlab/ci/processable_object_hierarchy.rb33
-rw-r--r--lib/gitlab/ci/project_config.rb52
-rw-r--r--lib/gitlab/ci/project_config/auto_devops.rb28
-rw-r--r--lib/gitlab/ci/project_config/bridge.rb19
-rw-r--r--lib/gitlab/ci/project_config/external_project.rb45
-rw-r--r--lib/gitlab/ci/project_config/parameter.rb21
-rw-r--r--lib/gitlab/ci/project_config/remote.rb21
-rw-r--r--lib/gitlab/ci/project_config/repository.rb32
-rw-r--r--lib/gitlab/ci/project_config/source.rb41
-rw-r--r--lib/gitlab/ci/reports/coverage_report_generator.rb2
-rw-r--r--lib/gitlab/ci/reports/sbom/component.rb8
-rw-r--r--lib/gitlab/ci/reports/sbom/report.rb4
-rw-r--r--lib/gitlab/ci/reports/sbom/source.rb8
-rw-r--r--lib/gitlab/ci/reports/security/scanner.rb4
-rw-r--r--lib/gitlab/ci/status/build/failed.rb4
-rw-r--r--lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/Jobs/Dependency-Scanning.latest.gitlab-ci.yml244
-rw-r--r--lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/Jobs/Deploy.latest.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/Jobs/License-Scanning.latest.gitlab-ci.yml48
-rw-r--r--lib/gitlab/ci/templates/Jobs/SAST.gitlab-ci.yml58
-rw-r--r--lib/gitlab/ci/templates/Jobs/SAST.latest.gitlab-ci.yml90
-rw-r--r--lib/gitlab/ci/templates/Katalon.gitlab-ci.yml65
-rw-r--r--lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml6
-rw-r--r--lib/gitlab/ci/templates/Security/Container-Scanning.latest.gitlab-ci.yml68
-rw-r--r--lib/gitlab/ci/templates/Terraform/Base.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/npm.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/trace.rb11
-rw-r--r--lib/gitlab/ci/variables/builder.rb4
-rw-r--r--lib/gitlab/ci/variables/helpers.rb28
-rw-r--r--lib/gitlab/ci/yaml_processor/feature_flags.rb27
-rw-r--r--lib/gitlab/ci/yaml_processor/result.rb10
-rw-r--r--lib/gitlab/cleanup/personal_access_tokens.rb105
-rw-r--r--lib/gitlab/closing_issue_extractor.rb6
-rw-r--r--lib/gitlab/cluster/lifecycle_events.rb5
-rw-r--r--lib/gitlab/config/entry/composable_hash.rb10
-rw-r--r--lib/gitlab/config/entry/validators.rb13
-rw-r--r--lib/gitlab/container_repository/tags/cache.rb4
-rw-r--r--lib/gitlab/data_builder/build.rb2
-rw-r--r--lib/gitlab/data_builder/pipeline.rb5
-rw-r--r--lib/gitlab/database.rb3
-rw-r--r--lib/gitlab/database/background_migration/batched_migration.rb14
-rw-r--r--lib/gitlab/database/background_migration/health_status.rb2
-rw-r--r--lib/gitlab/database/batch_average_counter.rb103
-rw-r--r--lib/gitlab/database/batch_count.rb6
-rw-r--r--lib/gitlab/database/batch_counter.rb31
-rw-r--r--lib/gitlab/database/gitlab_schemas.yml8
-rw-r--r--lib/gitlab/database/lock_writes_manager.rb45
-rw-r--r--lib/gitlab/database/migration_helpers.rb15
-rw-r--r--lib/gitlab/database/migrations/base_background_runner.rb2
-rw-r--r--lib/gitlab/database/migrations/test_background_runner.rb1
-rw-r--r--lib/gitlab/database/migrations/test_batched_background_runner.rb75
-rw-r--r--lib/gitlab/database/partitioning.rb12
-rw-r--r--lib/gitlab/database/partitioning/convert_table_to_first_list_partition.rb214
-rw-r--r--lib/gitlab/database/partitioning/partition_manager.rb25
-rw-r--r--lib/gitlab/database/partitioning/single_numeric_list_partition.rb20
-rw-r--r--lib/gitlab/database/partitioning/sliding_list_strategy.rb17
-rw-r--r--lib/gitlab/database/partitioning_migration_helpers/index_helpers.rb34
-rw-r--r--lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb48
-rw-r--r--lib/gitlab/database/postgres_constraint.rb29
-rw-r--r--lib/gitlab/database/query_analyzers/ci/partitioning_analyzer.rb41
-rw-r--r--lib/gitlab/database/query_analyzers/gitlab_schemas_metrics.rb2
-rw-r--r--lib/gitlab/database/query_analyzers/prevent_cross_database_modification.rb2
-rw-r--r--lib/gitlab/database/reflection.rb2
-rw-r--r--lib/gitlab/database/reindexing.rb2
-rw-r--r--lib/gitlab/database/tables_sorted_by_foreign_keys.rb41
-rw-r--r--lib/gitlab/database/tables_truncate.rb96
-rw-r--r--lib/gitlab/database_importers/security/training_providers/importer.rb42
-rw-r--r--lib/gitlab/diff/file_collection/compare.rb4
-rw-r--r--lib/gitlab/diff/highlight_cache.rb29
-rw-r--r--lib/gitlab/doorkeeper_secret_storing/pbkdf2_sha512.rb28
-rw-r--r--lib/gitlab/doorkeeper_secret_storing/secret/pbkdf2_sha512.rb38
-rw-r--r--lib/gitlab/doorkeeper_secret_storing/token/pbkdf2_sha512.rb30
-rw-r--r--lib/gitlab/email/attachment_uploader.rb4
-rw-r--r--lib/gitlab/email/message/in_product_marketing/team.rb14
-rw-r--r--lib/gitlab/email/message/repository_push.rb6
-rw-r--r--lib/gitlab/emoji.rb12
-rw-r--r--lib/gitlab/encoding_helper.rb24
-rw-r--r--lib/gitlab/etag_caching/middleware.rb12
-rw-r--r--lib/gitlab/etag_caching/store.rb4
-rw-r--r--lib/gitlab/experimentation.rb6
-rw-r--r--lib/gitlab/external_authorization/cache.rb6
-rw-r--r--lib/gitlab/fogbugz_import/importer.rb26
-rw-r--r--lib/gitlab/gfm/reference_rewriter.rb7
-rw-r--r--lib/gitlab/git.rb2
-rw-r--r--lib/gitlab/git/diff.rb1
-rw-r--r--lib/gitlab/git/repository.rb61
-rw-r--r--lib/gitlab/git/tag.rb2
-rw-r--r--lib/gitlab/git/wiki.rb24
-rw-r--r--lib/gitlab/git/wiki_page.rb34
-rw-r--r--lib/gitlab/gitaly_client/commit_service.rb29
-rw-r--r--lib/gitlab/gitaly_client/operation_service.rb22
-rw-r--r--lib/gitlab/gitaly_client/ref_service.rb32
-rw-r--r--lib/gitlab/gitaly_client/server_service.rb13
-rw-r--r--lib/gitlab/github_import/attachments_downloader.rb65
-rw-r--r--lib/gitlab/github_import/client.rb16
-rw-r--r--lib/gitlab/github_import/importer/events/base_importer.rb13
-rw-r--r--lib/gitlab/github_import/importer/events/changed_assignee.rb20
-rw-r--r--lib/gitlab/github_import/importer/events/changed_label.rb7
-rw-r--r--lib/gitlab/github_import/importer/events/changed_milestone.rb7
-rw-r--r--lib/gitlab/github_import/importer/events/changed_reviewer.rb54
-rw-r--r--lib/gitlab/github_import/importer/events/closed.rb9
-rw-r--r--lib/gitlab/github_import/importer/events/cross_referenced.rb2
-rw-r--r--lib/gitlab/github_import/importer/events/renamed.rb2
-rw-r--r--lib/gitlab/github_import/importer/events/reopened.rb9
-rw-r--r--lib/gitlab/github_import/importer/issue_event_importer.rb6
-rw-r--r--lib/gitlab/github_import/importer/protected_branch_importer.rb48
-rw-r--r--lib/gitlab/github_import/importer/protected_branches_importer.rb52
-rw-r--r--lib/gitlab/github_import/importer/release_attachments_importer.rb58
-rw-r--r--lib/gitlab/github_import/importer/releases_attachments_importer.rb59
-rw-r--r--lib/gitlab/github_import/importer/repository_importer.rb4
-rw-r--r--lib/gitlab/github_import/importer/single_endpoint_issue_events_importer.rb26
-rw-r--r--lib/gitlab/github_import/markdown_text.rb31
-rw-r--r--lib/gitlab/github_import/parallel_scheduling.rb8
-rw-r--r--lib/gitlab/github_import/representation/expose_attribute.rb4
-rw-r--r--lib/gitlab/github_import/representation/issue_event.rb9
-rw-r--r--lib/gitlab/github_import/representation/protected_branch.rb46
-rw-r--r--lib/gitlab/github_import/representation/release_attachments.rb44
-rw-r--r--lib/gitlab/github_import/sequential_importer.rb1
-rw-r--r--lib/gitlab/github_import/single_endpoint_notes_importing.rb28
-rw-r--r--lib/gitlab/github_import/user_finder.rb6
-rw-r--r--lib/gitlab/gon_helper.rb1
-rw-r--r--lib/gitlab/graphql/errors.rb1
-rw-r--r--lib/gitlab/graphql/limit/field_call_count.rb32
-rw-r--r--lib/gitlab/graphql/pagination/keyset/connection.rb28
-rw-r--r--lib/gitlab/graphql/pagination/keyset/last_items.rb25
-rw-r--r--lib/gitlab/graphql/type_name_deprecations.rb3
-rw-r--r--lib/gitlab/harbor/query.rb2
-rw-r--r--lib/gitlab/health_checks/gitaly_check.rb26
-rw-r--r--lib/gitlab/health_checks/redis.rb16
-rw-r--r--lib/gitlab/health_checks/redis/cache_check.rb11
-rw-r--r--lib/gitlab/health_checks/redis/queues_check.rb11
-rw-r--r--lib/gitlab/health_checks/redis/rate_limiting_check.rb11
-rw-r--r--lib/gitlab/health_checks/redis/redis_abstract_check.rb4
-rw-r--r--lib/gitlab/health_checks/redis/redis_check.rb38
-rw-r--r--lib/gitlab/health_checks/redis/sessions_check.rb11
-rw-r--r--lib/gitlab/health_checks/redis/shared_state_check.rb11
-rw-r--r--lib/gitlab/health_checks/redis/trace_chunks_check.rb11
-rw-r--r--lib/gitlab/hook_data/project_member_builder.rb20
-rw-r--r--lib/gitlab/i18n.rb18
-rw-r--r--lib/gitlab/import_export/attributes_finder.rb4
-rw-r--r--lib/gitlab/import_export/base/relation_factory.rb2
-rw-r--r--lib/gitlab/import_export/base/relation_object_saver.rb13
-rw-r--r--lib/gitlab/import_export/group/import_export.yml40
-rw-r--r--lib/gitlab/import_export/group/legacy_import_export.yml38
-rw-r--r--lib/gitlab/import_export/group/legacy_tree_restorer.rb22
-rw-r--r--lib/gitlab/import_export/group/relation_factory.rb16
-rw-r--r--lib/gitlab/import_export/group/relation_tree_restorer.rb4
-rw-r--r--lib/gitlab/import_export/group/tree_saver.rb3
-rw-r--r--lib/gitlab/import_export/json/streaming_serializer.rb44
-rw-r--r--lib/gitlab/import_export/legacy_relation_tree_saver.rb8
-rw-r--r--lib/gitlab/import_export/members_mapper.rb2
-rw-r--r--lib/gitlab/import_export/project/import_export.yml110
-rw-r--r--lib/gitlab/import_export/project/import_task.rb4
-rw-r--r--lib/gitlab/import_export/project/object_builder.rb14
-rw-r--r--lib/gitlab/import_export/project/relation_saver.rb3
-rw-r--r--lib/gitlab/import_export/project/relation_tree_restorer.rb2
-rw-r--r--lib/gitlab/import_export/project/tree_saver.rb3
-rw-r--r--lib/gitlab/instrumentation/redis.rb21
-rw-r--r--lib/gitlab/instrumentation/redis_base.rb14
-rw-r--r--lib/gitlab/instrumentation/redis_interceptor.rb57
-rw-r--r--lib/gitlab/issuable/clone/copy_resource_events_service.rb8
-rw-r--r--lib/gitlab/jira_import.rb5
-rw-r--r--lib/gitlab/kubernetes.rb8
-rw-r--r--lib/gitlab/legacy_github_import/client.rb14
-rw-r--r--lib/gitlab/legacy_github_import/project_creator.rb10
-rw-r--r--lib/gitlab/mailgun/webhook_processors/failure_logger.rb7
-rw-r--r--lib/gitlab/manifest_import/metadata.rb6
-rw-r--r--lib/gitlab/marginalia/comment.rb2
-rw-r--r--lib/gitlab/markdown_cache/redis/store.rb14
-rw-r--r--lib/gitlab/memory/jemalloc.rb10
-rw-r--r--lib/gitlab/memory/reports/jemalloc_stats.rb9
-rw-r--r--lib/gitlab/memory/watchdog.rb139
-rw-r--r--lib/gitlab/metrics/dashboard/stages/grafana_formatter.rb10
-rw-r--r--lib/gitlab/metrics/dashboard/transformers/yml/v1/prometheus_metrics.rb18
-rw-r--r--lib/gitlab/metrics/dashboard/validator/client.rb4
-rw-r--r--lib/gitlab/metrics/exporter/metrics_middleware.rb4
-rw-r--r--lib/gitlab/metrics/global_search_slis.rb114
-rw-r--r--lib/gitlab/metrics/samplers/puma_sampler.rb14
-rw-r--r--lib/gitlab/metrics/samplers/ruby_sampler.rb18
-rw-r--r--lib/gitlab/metrics/system.rb16
-rw-r--r--lib/gitlab/nav/top_nav_menu_builder.rb14
-rw-r--r--lib/gitlab/nav/top_nav_menu_header.rb14
-rw-r--r--lib/gitlab/nav/top_nav_menu_item.rb1
-rw-r--r--lib/gitlab/nav/top_nav_view_model_builder.rb7
-rw-r--r--lib/gitlab/no_cache_headers.rb4
-rw-r--r--lib/gitlab/pagination/gitaly_keyset_pager.rb4
-rw-r--r--lib/gitlab/pagination/keyset/column_order_definition.rb8
-rw-r--r--lib/gitlab/patch/sidekiq_cron_poller.rb21
-rw-r--r--lib/gitlab/quick_actions/issue_actions.rb63
-rw-r--r--lib/gitlab/quick_actions/merge_request_actions.rb39
-rw-r--r--lib/gitlab/quick_actions/timeline_text_and_date_time_separator.rb58
-rw-r--r--lib/gitlab/reactive_cache_set_cache.rb6
-rw-r--r--lib/gitlab/redis.rb19
-rw-r--r--lib/gitlab/redis/cache.rb2
-rw-r--r--lib/gitlab/redis/multi_store.rb2
-rw-r--r--lib/gitlab/repository_hash_cache.rb6
-rw-r--r--lib/gitlab/repository_set_cache.rb14
-rw-r--r--lib/gitlab/request_forgery_protection.rb2
-rw-r--r--lib/gitlab/seeder.rb42
-rw-r--r--lib/gitlab/seeders/ci/daily_build_group_report_result.rb47
-rw-r--r--lib/gitlab/set_cache.rb12
-rw-r--r--lib/gitlab/shell.rb4
-rw-r--r--lib/gitlab/sidekiq_daemon/memory_killer.rb6
-rw-r--r--lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job.rb10
-rw-r--r--lib/gitlab/sidekiq_middleware/server_metrics.rb26
-rw-r--r--lib/gitlab/sidekiq_versioning.rb6
-rw-r--r--lib/gitlab/slash_commands/presenters/base.rb20
-rw-r--r--lib/gitlab/spamcheck/client.rb49
-rw-r--r--lib/gitlab/subscription_portal.rb5
-rw-r--r--lib/gitlab/template/gitignore_template.rb2
-rw-r--r--lib/gitlab/tracking.rb2
-rw-r--r--lib/gitlab/tree_summary.rb4
-rw-r--r--lib/gitlab/uploads/migration_helper.rb38
-rw-r--r--lib/gitlab/usage/metrics/instrumentations/count_bulk_imports_entities_metric.rb11
-rw-r--r--lib/gitlab/usage/metrics/instrumentations/count_user_auth_metric.rb17
-rw-r--r--lib/gitlab/usage/metrics/instrumentations/redis_metric.rb26
-rw-r--r--lib/gitlab/usage_data.rb6
-rw-r--r--lib/gitlab/usage_data_counters.rb19
-rw-r--r--lib/gitlab/usage_data_counters/base_counter.rb4
-rw-r--r--lib/gitlab/usage_data_counters/hll_redis_counter.rb9
-rw-r--r--lib/gitlab/usage_data_counters/issue_activity_unique_counter.rb69
-rw-r--r--lib/gitlab/usage_data_counters/known_events/ci_templates.yml28
-rw-r--r--lib/gitlab/usage_data_counters/known_events/code_review_events.yml85
-rw-r--r--lib/gitlab/usage_data_counters/known_events/common.yml15
-rw-r--r--lib/gitlab/usage_data_counters/known_events/ecosystem.yml8
-rw-r--r--lib/gitlab/usage_data_counters/known_events/epic_board_events.yml19
-rw-r--r--lib/gitlab/usage_data_counters/known_events/kubernetes_agent.yml1
-rw-r--r--lib/gitlab/usage_data_counters/known_events/quickactions.yml12
-rw-r--r--lib/gitlab/usage_data_counters/merge_request_activity_unique_counter.rb10
-rw-r--r--lib/gitlab/usage_data_counters/merge_request_widget_extension_counter.rb2
-rw-r--r--lib/gitlab/utils/deep_size.rb4
-rw-r--r--lib/gitlab/utils/execution_tracker.rb25
-rw-r--r--lib/gitlab/view/presenter/base.rb7
-rw-r--r--lib/gitlab/visibility_level.rb8
-rw-r--r--lib/gitlab/web_hooks/recursion_detection.rb6
-rw-r--r--lib/gitlab/workhorse.rb5
-rw-r--r--lib/gitlab_edition.rb15
-rw-r--r--lib/google_api/cloud_platform/client.rb2
-rw-r--r--lib/learn_gitlab/onboarding.rb69
-rw-r--r--lib/learn_gitlab/project.rb38
-rw-r--r--lib/object_storage/direct_upload.rb2
-rw-r--r--lib/omni_auth/strategies/bitbucket.rb2
-rw-r--r--lib/peek/views/redis_detailed.rb6
-rw-r--r--lib/product_analytics/event_params.rb68
-rw-r--r--lib/security/weak_passwords.rb88
-rw-r--r--lib/sidebars/groups/menus/observability_menu.rb34
-rw-r--r--lib/sidebars/groups/menus/packages_registries_menu.rb6
-rw-r--r--lib/sidebars/groups/menus/settings_menu.rb31
-rw-r--r--lib/sidebars/groups/panel.rb1
-rw-r--r--lib/sidebars/projects/menus/infrastructure_menu.rb12
-rw-r--r--lib/sidebars/projects/menus/learn_gitlab_menu.rb4
-rw-r--r--lib/sidebars/projects/menus/merge_requests_menu.rb4
-rw-r--r--lib/sidebars/projects/menus/monitor_menu.rb10
-rw-r--r--lib/sidebars/projects/menus/packages_registries_menu.rb9
-rw-r--r--lib/sidebars/projects/menus/settings_menu.rb16
-rw-r--r--lib/sidebars/projects/panel.rb3
-rw-r--r--lib/tasks/gitlab/assets.rake11
-rw-r--r--lib/tasks/gitlab/db/truncate_legacy_tables.rake31
-rw-r--r--lib/tasks/gitlab/db/validate_config.rake2
-rw-r--r--lib/tasks/gitlab/import_export/export.rake6
-rw-r--r--lib/tasks/gitlab/import_export/import.rake6
-rw-r--r--lib/tasks/gitlab/tw/codeowners.rake10
-rw-r--r--lib/tasks/gitlab/uploads/migrate.rake22
-rw-r--r--lib/tasks/gitlab/usage_data.rake22
-rw-r--r--lib/tasks/haml-lint.rake7
-rw-r--r--lib/tasks/rubocop.rake13
-rw-r--r--lib/tasks/tanuki_emoji.rake18
430 files changed, 20113 insertions, 2120 deletions
diff --git a/lib/api/admin/batched_background_migrations.rb b/lib/api/admin/batched_background_migrations.rb
new file mode 100644
index 00000000000..675f3365bd3
--- /dev/null
+++ b/lib/api/admin/batched_background_migrations.rb
@@ -0,0 +1,108 @@
+# frozen_string_literal: true
+
+module API
+ module Admin
+ class BatchedBackgroundMigrations < ::API::Base
+ feature_category :database
+ urgency :low
+
+ before do
+ authenticated_as_admin!
+ end
+
+ namespace 'admin' do
+ resources 'batched_background_migrations/:id' do
+ desc 'Retrieve a batched background migration'
+ params do
+ optional :database,
+ type: String,
+ values: Gitlab::Database.all_database_names,
+ desc: 'The name of the database',
+ default: 'main'
+ requires :id,
+ type: Integer,
+ desc: 'The batched background migration id'
+ end
+ get do
+ Gitlab::Database::SharedModel.using_connection(base_model.connection) do
+ present_entity(batched_background_migration)
+ end
+ end
+ end
+
+ resources 'batched_background_migrations' do
+ desc 'Get the list of the batched background migrations'
+ params do
+ optional :database,
+ type: String,
+ values: Gitlab::Database.all_database_names,
+ desc: 'The name of the database, the default `main`',
+ default: 'main'
+ end
+ get do
+ Gitlab::Database::SharedModel.using_connection(base_model.connection) do
+ migrations = Database::BatchedBackgroundMigrationsFinder.new(connection: base_model.connection).execute
+ present_entity(migrations)
+ end
+ end
+ end
+
+ resources 'batched_background_migrations/:id/resume' do
+ desc 'Resume a batched background migration'
+ params do
+ optional :database,
+ type: String,
+ values: Gitlab::Database.all_database_names,
+ desc: 'The name of the database',
+ default: 'main'
+ requires :id,
+ type: Integer,
+ desc: 'The batched background migration id'
+ end
+ put do
+ Gitlab::Database::SharedModel.using_connection(base_model.connection) do
+ batched_background_migration.execute!
+ present_entity(batched_background_migration)
+ end
+ end
+ end
+
+ resources 'batched_background_migrations/:id/pause' do
+ desc 'Pause a batched background migration'
+ params do
+ optional :database,
+ type: String,
+ values: Gitlab::Database.all_database_names,
+ desc: 'The name of the database',
+ default: 'main'
+ requires :id,
+ type: Integer,
+ desc: 'The batched background migration id'
+ end
+ put do
+ Gitlab::Database::SharedModel.using_connection(base_model.connection) do
+ batched_background_migration.pause!
+ present_entity(batched_background_migration)
+ end
+ end
+ end
+ end
+
+ helpers do
+ def batched_background_migration
+ @batched_background_migration ||= Gitlab::Database::BackgroundMigration::BatchedMigration.find(params[:id])
+ end
+
+ def base_model
+ database = params[:database] || Gitlab::Database::MAIN_DATABASE_NAME
+ @base_model ||= Gitlab::Database.database_base_models[database]
+ end
+
+ def present_entity(result)
+ present result,
+ with: ::API::Entities::BatchedBackgroundMigration
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/api.rb b/lib/api/api.rb
index e4158eee37f..443bf1d649a 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -167,6 +167,7 @@ module API
# Keep in alphabetical order
mount ::API::AccessRequests
+ mount ::API::Admin::BatchedBackgroundMigrations
mount ::API::Admin::Ci::Variables
mount ::API::Admin::InstanceClusters
mount ::API::Admin::PlanLimits
@@ -237,7 +238,6 @@ module API
mount ::API::ImportGithub
mount ::API::Integrations
mount ::API::Integrations::JiraConnect::Subscriptions
- mount ::API::Integrations::Slack::Events
mount ::API::Invitations
mount ::API::IssueLinks
mount ::API::Issues
@@ -263,6 +263,7 @@ module API
mount ::API::PackageFiles
mount ::API::Pages
mount ::API::PagesDomains
+ mount ::API::PersonalAccessTokens::SelfRevocation
mount ::API::PersonalAccessTokens
mount ::API::ProjectClusters
mount ::API::ProjectContainerRepositories
@@ -290,6 +291,7 @@ module API
mount ::API::ResourceLabelEvents
mount ::API::ResourceMilestoneEvents
mount ::API::ResourceStateEvents
+ mount ::API::RpmProjectPackages
mount ::API::RubygemPackages
mount ::API::Search
mount ::API::Settings
@@ -316,6 +318,7 @@ module API
mount ::API::Users
mount ::API::Version
mount ::API::Wikis
+ mount ::API::Ml::Mlflow
end
mount ::API::Internal::Base
diff --git a/lib/api/branches.rb b/lib/api/branches.rb
index b8444351029..5588818cbaf 100644
--- a/lib/api/branches.rb
+++ b/lib/api/branches.rb
@@ -52,25 +52,21 @@ module API
merged_branch_names = repository.merged_branch_names(branches.map(&:name))
- if Feature.enabled?(:api_caching_branches, user_project, type: :development)
- present_cached(
- branches,
- with: Entities::Branch,
- current_user: current_user,
- project: user_project,
- merged_branch_names: merged_branch_names,
- expires_in: 10.minutes,
- cache_context: -> (branch) { [current_user&.cache_key, merged_branch_names.include?(branch.name)] }
- )
- else
- present(
- branches,
- with: Entities::Branch,
- current_user: current_user,
- project: user_project,
- merged_branch_names: merged_branch_names
- )
- end
+ expiry_time = if Feature.enabled?(:increase_branch_cache_expiry, type: :ops)
+ 60.minutes
+ else
+ 10.minutes
+ end
+
+ present_cached(
+ branches,
+ with: Entities::Branch,
+ current_user: current_user,
+ project: user_project,
+ merged_branch_names: merged_branch_names,
+ expires_in: expiry_time,
+ cache_context: -> (branch) { [current_user&.cache_key, merged_branch_names.include?(branch.name)] }
+ )
end
end
@@ -146,7 +142,8 @@ module API
branch = find_branch!(params[:branch])
protected_branch = user_project.protected_branches.find_by(name: branch.name)
- protected_branch&.destroy
+
+ ::ProtectedBranches::DestroyService.new(user_project, current_user).execute(protected_branch) if protected_branch
present branch, with: Entities::Branch, current_user: current_user, project: user_project
end
diff --git a/lib/api/ci/job_artifacts.rb b/lib/api/ci/job_artifacts.rb
index b843404e9d7..b3a0a9ef54a 100644
--- a/lib/api/ci/job_artifacts.rb
+++ b/lib/api/ci/job_artifacts.rb
@@ -143,7 +143,7 @@ module API
reject_if_build_artifacts_size_refreshing!(build.project)
- build.erase_erasable_artifacts!
+ ::Ci::JobArtifacts::DeleteService.new(build).execute
status :no_content
end
diff --git a/lib/api/ci/jobs.rb b/lib/api/ci/jobs.rb
index cd5f1f77ced..6049993bf6f 100644
--- a/lib/api/ci/jobs.rb
+++ b/lib/api/ci/jobs.rb
@@ -142,7 +142,8 @@ module API
reject_if_build_artifacts_size_refreshing!(build.project)
- build.erase(erased_by: current_user)
+ ::Ci::BuildEraseService.new(build, current_user).execute
+
present build, with: Entities::Ci::Job
end
@@ -209,8 +210,8 @@ module API
.select { |_role, role_access_level| role_access_level <= user_access_level }
.map(&:first)
- environment = if environment_slug = current_authenticated_job.persisted_environment&.slug
- { slug: environment_slug }
+ environment = if persisted_environment = current_authenticated_job.persisted_environment
+ { tier: persisted_environment.tier, slug: persisted_environment.slug }
end
# See https://gitlab.com/gitlab-org/cluster-integration/gitlab-agent/-/blob/master/doc/kubernetes_ci_access.md#apiv4joballowed_agents-api
diff --git a/lib/api/ci/runners.rb b/lib/api/ci/runners.rb
index ec9b09a3419..4b578f8b7e5 100644
--- a/lib/api/ci/runners.rb
+++ b/lib/api/ci/runners.rb
@@ -93,7 +93,7 @@ module API
params[:active] = !params.delete(:paused) if params.include?(:paused)
update_service = ::Ci::Runners::UpdateRunnerService.new(runner)
- if update_service.update(declared_params(include_missing: false))
+ if update_service.execute(declared_params(include_missing: false)).success?
present runner, with: Entities::Ci::RunnerDetails, current_user: current_user
else
render_validation_error!(runner)
@@ -129,8 +129,17 @@ module API
authenticate_list_runners_jobs!(runner)
jobs = ::Ci::RunnerJobsFinder.new(runner, current_user, params).execute
+ jobs = jobs.preload( # rubocop: disable CodeReuse/ActiveRecord
+ [
+ :user,
+ { pipeline: { project: [:route, { namespace: :route }] } },
+ { project: [:route, { namespace: :route }] }
+ ]
+ )
+ jobs = paginate(jobs)
+ jobs.each(&:commit) # batch loads all commits in the page
- present paginate(jobs), with: Entities::Ci::JobBasicWithProject
+ present jobs, with: Entities::Ci::JobBasicWithProject
end
desc 'Reset runner authentication token' do
@@ -352,7 +361,7 @@ module API
def authenticate_list_runners_jobs!(runner)
return if current_user.admin?
- forbidden!("No access granted") unless can?(current_user, :read_runner, runner)
+ forbidden!("No access granted") unless can?(current_user, :read_builds, runner)
end
end
end
diff --git a/lib/api/composer_packages.rb b/lib/api/composer_packages.rb
index de59cb4a7c3..d9806fa37d1 100644
--- a/lib/api/composer_packages.rb
+++ b/lib/api/composer_packages.rb
@@ -150,17 +150,18 @@ module API
get 'archives/*package_name', urgency: :default do
authorize_read_package!(authorized_user_project)
- metadata = authorized_user_project
+ package = authorized_user_project
.packages
.composer
.with_name(params[:package_name])
.with_composer_target(params[:sha])
.first
- &.composer_metadatum
+ metadata = package&.composer_metadatum
not_found! unless metadata
track_package_event('pull_package', :composer, project: authorized_user_project, namespace: authorized_user_project.namespace)
+ package.touch_last_downloaded_at
send_git_archive authorized_user_project.repository, ref: metadata.target_sha, format: 'zip', append_sha: true
end
diff --git a/lib/api/concerns/packages/conan_endpoints.rb b/lib/api/concerns/packages/conan_endpoints.rb
index a90269b565c..d8c2eb4ff33 100644
--- a/lib/api/concerns/packages/conan_endpoints.rb
+++ b/lib/api/concerns/packages/conan_endpoints.rb
@@ -135,7 +135,7 @@ module API
route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true
get 'packages/:conan_package_reference', urgency: :low do
- authorize!(:read_package, project)
+ authorize_read_package!(project)
presenter = ::Packages::Conan::PackagePresenter.new(
package,
@@ -154,7 +154,7 @@ module API
route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true
get urgency: :low do
- authorize!(:read_package, project)
+ authorize_read_package!(project)
presenter = ::Packages::Conan::PackagePresenter.new(package, current_user, project)
@@ -237,7 +237,7 @@ module API
route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true
post 'packages/:conan_package_reference/upload_urls', urgency: :low do
- authorize!(:read_package, project)
+ authorize_read_package!(project)
status 200
present package_upload_urls, with: ::API::Entities::ConanPackage::ConanUploadUrls
@@ -250,7 +250,7 @@ module API
route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true
post 'upload_urls', urgency: :low do
- authorize!(:read_package, project)
+ authorize_read_package!(project)
status 200
present recipe_upload_urls, with: ::API::Entities::ConanPackage::ConanUploadUrls
diff --git a/lib/api/concerns/packages/debian_package_endpoints.rb b/lib/api/concerns/packages/debian_package_endpoints.rb
index e8d27448f02..2883944a745 100644
--- a/lib/api/concerns/packages/debian_package_endpoints.rb
+++ b/lib/api/concerns/packages/debian_package_endpoints.rb
@@ -35,12 +35,30 @@ module API
::Packages::Debian::DistributionsFinder.new(container, codename_or_suite: params[:distribution]).execute.last!
end
- def present_package_file!
+ def present_distribution_package_file!
not_found! unless params[:package_name].start_with?(params[:letter])
package_file = distribution_from!(user_project).package_files.with_file_name(params[:file_name]).last!
- present_carrierwave_file!(package_file.file)
+ present_package_file!(package_file)
+ end
+
+ def present_index_file!(file_type)
+ relation = "::Packages::Debian::#{project_or_group.class.name}ComponentFile".constantize
+
+ relation = relation
+ .preload_distribution
+ .with_container(project_or_group)
+ .with_codename_or_suite(params[:distribution])
+ .with_component_name(params[:component])
+ .with_file_type(file_type)
+ .with_architecture_name(params[:architecture])
+ .with_compression_type(nil)
+ .order_created_asc
+
+ relation = relation.with_file_sha256(params[:file_sha256]) if params[:file_sha256]
+
+ present_carrierwave_file!(relation.last!.file)
end
end
@@ -66,6 +84,7 @@ module API
namespace 'dists/*distribution', requirements: DISTRIBUTION_REQUIREMENTS do
# GET {projects|groups}/:id/packages/debian/dists/*distribution/Release.gpg
+ # https://wiki.debian.org/DebianRepository/Format#A.22Release.22_files
desc 'The Release file signature' do
detail 'This feature was introduced in GitLab 13.5'
end
@@ -76,6 +95,7 @@ module API
end
# GET {projects|groups}/:id/packages/debian/dists/*distribution/Release
+ # https://wiki.debian.org/DebianRepository/Format#A.22Release.22_files
desc 'The unsigned Release file' do
detail 'This feature was introduced in GitLab 13.5'
end
@@ -86,6 +106,7 @@ module API
end
# GET {projects|groups}/:id/packages/debian/dists/*distribution/InRelease
+ # https://wiki.debian.org/DebianRepository/Format#A.22Release.22_files
desc 'The signed Release file' do
detail 'This feature was introduced in GitLab 13.5'
end
@@ -97,31 +118,87 @@ module API
params do
requires :component, type: String, desc: 'The Debian Component', regexp: Gitlab::Regex.debian_component_regex
- requires :architecture, type: String, desc: 'The Debian Architecture', regexp: Gitlab::Regex.debian_architecture_regex
end
- namespace ':component/binary-:architecture', requirements: COMPONENT_ARCHITECTURE_REQUIREMENTS do
- # GET {projects|groups}/:id/packages/debian/dists/*distribution/:component/binary-:architecture/Packages
- desc 'The binary files index' do
- detail 'This feature was introduced in GitLab 13.5'
+ namespace ':component', requirements: COMPONENT_ARCHITECTURE_REQUIREMENTS do
+ params do
+ requires :architecture, type: String, desc: 'The Debian Architecture', regexp: Gitlab::Regex.debian_architecture_regex
+ end
+
+ namespace 'debian-installer/binary-:architecture' do
+ # GET {projects|groups}/:id/packages/debian/dists/*distribution/:component/debian-installer/binary-:architecture/Packages
+ # https://wiki.debian.org/DebianRepository/Format#A.22Packages.22_Indices
+ desc 'The installer (udeb) binary files index' do
+ detail 'This feature was introduced in GitLab 15.4'
+ end
+
+ route_setting :authentication, authenticate_non_public: true
+ get 'Packages' do
+ present_index_file!(:di_packages)
+ end
+
+ # GET {projects|groups}/:id/packages/debian/dists/*distribution/:component/debian-installer/binary-:architecture/by-hash/SHA256/:file_sha256
+ # https://wiki.debian.org/DebianRepository/Format?action=show&redirect=RepositoryFormat#indices_acquisition_via_hashsums_.28by-hash.29
+ desc 'The installer (udeb) binary files index by hash' do
+ detail 'This feature was introduced in GitLab 15.4'
+ end
+
+ route_setting :authentication, authenticate_non_public: true
+ get 'by-hash/SHA256/:file_sha256' do
+ present_index_file!(:di_packages)
+ end
+ end
+
+ namespace 'source', requirements: COMPONENT_ARCHITECTURE_REQUIREMENTS do
+ # GET {projects|groups}/:id/packages/debian/dists/*distribution/:component/source/Sources
+ # https://wiki.debian.org/DebianRepository/Format#A.22Sources.22_Indices
+ desc 'The source files index' do
+ detail 'This feature was introduced in GitLab 15.4'
+ end
+
+ route_setting :authentication, authenticate_non_public: true
+ get 'Sources' do
+ present_index_file!(:sources)
+ end
+
+ # GET {projects|groups}/:id/packages/debian/dists/*distribution/:component/source/by-hash/SHA256/:file_sha256
+ # https://wiki.debian.org/DebianRepository/Format?action=show&redirect=RepositoryFormat#indices_acquisition_via_hashsums_.28by-hash.29
+ desc 'The source files index by hash' do
+ detail 'This feature was introduced in GitLab 15.4'
+ end
+
+ route_setting :authentication, authenticate_non_public: true
+ get 'by-hash/SHA256/:file_sha256' do
+ present_index_file!(:sources)
+ end
+ end
+
+ params do
+ requires :architecture, type: String, desc: 'The Debian Architecture', regexp: Gitlab::Regex.debian_architecture_regex
end
- route_setting :authentication, authenticate_non_public: true
- get 'Packages' do
- relation = "::Packages::Debian::#{project_or_group.class.name}ComponentFile".constantize
-
- component_file = relation
- .preload_distribution
- .with_container(project_or_group)
- .with_codename_or_suite(params[:distribution])
- .with_component_name(params[:component])
- .with_file_type(:packages)
- .with_architecture_name(params[:architecture])
- .with_compression_type(nil)
- .order_created_asc
- .last!
-
- present_carrierwave_file!(component_file.file)
+ namespace 'binary-:architecture', requirements: COMPONENT_ARCHITECTURE_REQUIREMENTS do
+ # GET {projects|groups}/:id/packages/debian/dists/*distribution/:component/binary-:architecture/Packages
+ # https://wiki.debian.org/DebianRepository/Format#A.22Packages.22_Indices
+ desc 'The binary files index' do
+ detail 'This feature was introduced in GitLab 13.5'
+ end
+
+ route_setting :authentication, authenticate_non_public: true
+ get 'Packages' do
+ present_index_file!(:packages)
+ end
+
+ # GET {projects|groups}/:id/packages/debian/dists/*distribution/:component/binary-:architecture/by-hash/SHA256/:file_sha256
+ # https://wiki.debian.org/DebianRepository/Format?action=show&redirect=RepositoryFormat#indices_acquisition_via_hashsums_.28by-hash.29
+ desc 'The binary files index by hash' do
+ detail 'This feature was introduced in GitLab 15.4'
+ end
+
+ route_setting :authentication, authenticate_non_public: true
+ get 'by-hash/SHA256/:file_sha256' do
+ present_index_file!(:packages)
+ end
end
end
end
diff --git a/lib/api/debian_group_packages.rb b/lib/api/debian_group_packages.rb
index 8bf4ac22802..0962d749558 100644
--- a/lib/api/debian_group_packages.rb
+++ b/lib/api/debian_group_packages.rb
@@ -48,7 +48,7 @@ module API
route_setting :authentication, authenticate_non_public: true
get 'pool/:distribution/:project_id/:letter/:package_name/:package_version/:file_name', requirements: PACKAGE_FILE_REQUIREMENTS do
- present_package_file!
+ present_distribution_package_file!
end
end
end
diff --git a/lib/api/debian_project_packages.rb b/lib/api/debian_project_packages.rb
index 06846d8f36e..9dedc4390f7 100644
--- a/lib/api/debian_project_packages.rb
+++ b/lib/api/debian_project_packages.rb
@@ -51,7 +51,7 @@ module API
route_setting :authentication, authenticate_non_public: true
get 'pool/:distribution/:letter/:package_name/:package_version/:file_name', requirements: PACKAGE_FILE_REQUIREMENTS do
- present_package_file!
+ present_distribution_package_file!
end
params do
diff --git a/lib/api/entities/batched_background_migration.rb b/lib/api/entities/batched_background_migration.rb
new file mode 100644
index 00000000000..eba17ff98f4
--- /dev/null
+++ b/lib/api/entities/batched_background_migration.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class BatchedBackgroundMigration < Grape::Entity
+ expose :id
+ expose :job_class_name
+ expose :table_name
+ expose :status, &:status_name
+ expose :progress
+ expose :created_at
+ end
+ end
+end
diff --git a/lib/api/entities/ci/job_basic.rb b/lib/api/entities/ci/job_basic.rb
index 0badde4089e..3d9318ec428 100644
--- a/lib/api/entities/ci/job_basic.rb
+++ b/lib/api/entities/ci/job_basic.rb
@@ -18,6 +18,12 @@ module API
expose :web_url do |job, _options|
Gitlab::Routing.url_helpers.project_job_url(job.project, job)
end
+
+ expose :project do
+ expose :ci_job_token_scope_enabled do |job|
+ job.project.ci_job_token_scope_enabled?
+ end
+ end
end
end
end
diff --git a/lib/api/entities/ci/job_request/image.rb b/lib/api/entities/ci/job_request/image.rb
index 83f64da6050..92d68269265 100644
--- a/lib/api/entities/ci/job_request/image.rb
+++ b/lib/api/entities/ci/job_request/image.rb
@@ -8,7 +8,7 @@ module API
expose :name, :entrypoint
expose :ports, using: Entities::Ci::JobRequest::Port
- expose :pull_policy, if: ->(_) { ::Feature.enabled?(:ci_docker_image_pull_policy) }
+ expose :pull_policy
end
end
end
diff --git a/lib/api/entities/ci/job_request/service.rb b/lib/api/entities/ci/job_request/service.rb
index 7d494c7e516..128591058fe 100644
--- a/lib/api/entities/ci/job_request/service.rb
+++ b/lib/api/entities/ci/job_request/service.rb
@@ -8,7 +8,7 @@ module API
expose :name, :entrypoint
expose :ports, using: Entities::Ci::JobRequest::Port
- expose :pull_policy, if: ->(_) { ::Feature.enabled?(:ci_docker_image_pull_policy) }
+ expose :pull_policy
expose :alias, :command
expose :variables
end
diff --git a/lib/api/entities/merge_request_reviewer.rb b/lib/api/entities/merge_request_reviewer.rb
index 3bf2ccc36aa..a47321ef929 100644
--- a/lib/api/entities/merge_request_reviewer.rb
+++ b/lib/api/entities/merge_request_reviewer.rb
@@ -4,7 +4,6 @@ module API
module Entities
class MergeRequestReviewer < Grape::Entity
expose :reviewer, as: :user, using: Entities::UserBasic
- expose :updated_state_by, using: Entities::UserBasic
expose :state
expose :created_at
end
diff --git a/lib/api/entities/ml/mlflow/experiment.rb b/lib/api/entities/ml/mlflow/experiment.rb
new file mode 100644
index 00000000000..cfe366feaab
--- /dev/null
+++ b/lib/api/entities/ml/mlflow/experiment.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ module Ml
+ module Mlflow
+ class Experiment < Grape::Entity
+ expose :experiment do
+ expose :experiment_id
+ expose :name
+ expose :lifecycle_stage
+ expose :artifact_location
+ end
+
+ private
+
+ def lifecycle_stage
+ object.deleted_on? ? 'deleted' : 'active'
+ end
+
+ def experiment_id
+ object.iid.to_s
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/ml/mlflow/new_experiment.rb b/lib/api/entities/ml/mlflow/new_experiment.rb
new file mode 100644
index 00000000000..09791839850
--- /dev/null
+++ b/lib/api/entities/ml/mlflow/new_experiment.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ module Ml
+ module Mlflow
+ class NewExperiment < Grape::Entity
+ expose :experiment_id
+
+ private
+
+ def experiment_id
+ object.iid.to_s
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/ml/mlflow/run.rb b/lib/api/entities/ml/mlflow/run.rb
new file mode 100644
index 00000000000..c679330206e
--- /dev/null
+++ b/lib/api/entities/ml/mlflow/run.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ module Ml
+ module Mlflow
+ class Run < Grape::Entity
+ expose :run do
+ expose(:info) { |candidate| RunInfo.represent(candidate) }
+ expose(:data) { |candidate| {} }
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/ml/mlflow/run_info.rb b/lib/api/entities/ml/mlflow/run_info.rb
new file mode 100644
index 00000000000..096950e349d
--- /dev/null
+++ b/lib/api/entities/ml/mlflow/run_info.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ module Ml
+ module Mlflow
+ class RunInfo < Grape::Entity
+ expose :run_id
+ expose :run_id, as: :run_uuid
+ expose(:experiment_id) { |candidate| candidate.experiment.iid.to_s }
+ expose(:start_time) { |candidate| candidate.start_time || 0 }
+ expose :end_time, expose_nil: false
+ expose(:status) { |candidate| candidate.status.to_s.upcase }
+ expose(:artifact_uri) { |candidate| 'not_implemented' }
+ expose(:lifecycle_stage) { |candidate| 'active' }
+ expose(:user_id) { |candidate| candidate.user_id.to_s }
+
+ private
+
+ def run_id
+ object.iid.to_s
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/ml/mlflow/update_run.rb b/lib/api/entities/ml/mlflow/update_run.rb
new file mode 100644
index 00000000000..5acdaab0e33
--- /dev/null
+++ b/lib/api/entities/ml/mlflow/update_run.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ module Ml
+ module Mlflow
+ class UpdateRun < Grape::Entity
+ expose :run_info
+
+ private
+
+ def run_info
+ ::API::Entities::Ml::Mlflow::RunInfo.represent object
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/package.rb b/lib/api/entities/package.rb
index 1efd457aa5f..18fc0576dd4 100644
--- a/lib/api/entities/package.rb
+++ b/lib/api/entities/package.rb
@@ -39,6 +39,7 @@ module API
end
expose :created_at
+ expose :last_downloaded_at
expose :project_id, if: ->(_, opts) { opts[:group] }
expose :project_path, if: ->(obj, opts) { opts[:group] && Ability.allowed?(opts[:user], :read_project, obj.project) }
expose :tags
diff --git a/lib/api/entities/personal_access_token_with_details.rb b/lib/api/entities/personal_access_token_with_details.rb
deleted file mode 100644
index 5654bd4a1e1..00000000000
--- a/lib/api/entities/personal_access_token_with_details.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-# frozen_string_literal: true
-
-module API
- module Entities
- class PersonalAccessTokenWithDetails < Entities::PersonalAccessToken
- expose :expired?, as: :expired
- expose :expires_soon?, as: :expires_soon
- expose :revoke_path do |token|
- Gitlab::Routing.url_helpers.revoke_profile_personal_access_token_path(token)
- end
- end
- end
-end
diff --git a/lib/api/entities/user_safe.rb b/lib/api/entities/user_safe.rb
index fb99c2e960d..127a8ef2160 100644
--- a/lib/api/entities/user_safe.rb
+++ b/lib/api/entities/user_safe.rb
@@ -3,9 +3,13 @@
module API
module Entities
class UserSafe < Grape::Entity
+ include RequestAwareEntity
+
expose :id, :username
expose :name do |user|
- user.redacted_name(options[:current_user])
+ current_user = request.respond_to?(:current_user) ? request.current_user : options.fetch(:current_user, nil)
+
+ user.redacted_name(current_user)
end
end
end
diff --git a/lib/api/generic_packages.rb b/lib/api/generic_packages.rb
index 0b1c06b3c26..ad5455c5de6 100644
--- a/lib/api/generic_packages.rb
+++ b/lib/api/generic_packages.rb
@@ -102,7 +102,7 @@ module API
track_package_event('pull_package', :generic, project: project, user: current_user, namespace: project.namespace)
- present_carrierwave_file!(package_file.file)
+ present_package_file!(package_file)
end
end
end
diff --git a/lib/api/groups.rb b/lib/api/groups.rb
index 82bbab5d7d4..6b1fc0d4279 100644
--- a/lib/api/groups.rb
+++ b/lib/api/groups.rb
@@ -96,9 +96,9 @@ module API
present options[:with].prepare_relation(projects, options), options
end
- def present_groups(params, groups)
+ def present_groups(params, groups, serializer: Entities::Group)
options = {
- with: Entities::Group,
+ with: serializer,
current_user: current_user,
statistics: params[:statistics] && current_user&.admin?
}
@@ -248,6 +248,8 @@ module API
authorize! :admin_group, group
+ group.remove_avatar! if params.key?(:avatar) && params[:avatar].nil?
+
if update_group(group)
present_group_details(params, group, with_projects: true)
else
@@ -392,6 +394,21 @@ module API
end
end
+ desc 'Get the groups to where the current group can be transferred to'
+ params do
+ optional :search, type: String, desc: 'Return list of namespaces matching the search criteria'
+ use :pagination
+ end
+ get ':id/transfer_locations', feature_category: :subgroups do
+ authorize! :admin_group, user_group
+ args = declared_params(include_missing: false)
+
+ groups = ::Groups::AcceptingGroupTransfersFinder.new(current_user, user_group, args).execute
+ groups = groups.with_route
+
+ present_groups params, groups, serializer: Entities::PublicGroupDetails
+ end
+
desc 'Transfer a group to a new parent group or promote a subgroup to a root group'
params do
optional :group_id,
diff --git a/lib/api/helm_packages.rb b/lib/api/helm_packages.rb
index a1b265bc8f3..f90084a7e57 100644
--- a/lib/api/helm_packages.rb
+++ b/lib/api/helm_packages.rb
@@ -67,7 +67,7 @@ module API
track_package_event('pull_package', :helm, project: authorized_user_project, namespace: authorized_user_project.namespace)
- present_carrierwave_file!(package_file.file)
+ present_package_file!(package_file)
end
desc 'Authorize a chart upload from workhorse' do
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index 1d0f0c6e7bb..e29d76a5950 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -7,9 +7,12 @@ module API
include Helpers::Pagination
include Helpers::PaginationStrategies
include Gitlab::Ci::Artifacts::Logger
+ include Gitlab::Utils::StrongMemoize
SUDO_HEADER = "HTTP_SUDO"
GITLAB_SHARED_SECRET_HEADER = "Gitlab-Shared-Secret"
+ GITLAB_SHELL_API_HEADER = "Gitlab-Shell-Api-Request"
+ GITLAB_SHELL_JWT_ISSUER = "gitlab-shell"
SUDO_PARAM = :sudo
API_USER_ENV = 'gitlab.api.user'
API_TOKEN_ENV = 'gitlab.api.token'
@@ -283,12 +286,22 @@ module API
end
def authenticate_by_gitlab_shell_token!
- input = params['secret_token']
- input ||= Base64.decode64(headers[GITLAB_SHARED_SECRET_HEADER]) if headers.key?(GITLAB_SHARED_SECRET_HEADER)
+ if Feature.enabled?(:gitlab_shell_jwt_token)
+ begin
+ payload, _ = JSONWebToken::HMACToken.decode(headers[GITLAB_SHELL_API_HEADER], secret_token)
+ unauthorized! unless payload['iss'] == GITLAB_SHELL_JWT_ISSUER
+ rescue JWT::DecodeError, JWT::ExpiredSignature, JWT::ImmatureSignature => ex
+ Gitlab::ErrorTracking.track_exception(ex)
+ unauthorized!
+ end
+ else
+ input = params['secret_token']
+ input ||= Base64.decode64(headers[GITLAB_SHARED_SECRET_HEADER]) if headers.key?(GITLAB_SHARED_SECRET_HEADER)
- input&.chomp!
+ input&.chomp!
- unauthorized! unless Devise.secure_compare(secret_token, input)
+ unauthorized! unless Devise.secure_compare(secret_token, input)
+ end
end
def authenticated_with_can_read_all_resources!
@@ -719,7 +732,13 @@ module API
end
def secret_token
- Gitlab::Shell.secret_token
+ if Feature.enabled?(:gitlab_shell_jwt_token)
+ strong_memoize(:secret_token) do
+ File.read(Gitlab.config.gitlab_shell.secret_file)
+ end
+ else
+ Gitlab::Shell.secret_token
+ end
end
def authenticate_non_public?
diff --git a/lib/api/helpers/groups_helpers.rb b/lib/api/helpers/groups_helpers.rb
index 2b10eebb009..e9af50b80be 100644
--- a/lib/api/helpers/groups_helpers.rb
+++ b/lib/api/helpers/groups_helpers.rb
@@ -11,8 +11,7 @@ module API
optional :visibility, type: String,
values: Gitlab::VisibilityLevel.string_values,
desc: 'The visibility of the group'
- # TODO: remove rubocop disable - https://gitlab.com/gitlab-org/gitlab/issues/14960
- optional :avatar, type: File, desc: 'Avatar image for the group' # rubocop:disable Scalability/FileUploads
+ optional :avatar, type: ::API::Validations::Types::WorkhorseFile, desc: 'Avatar image for the group'
optional :share_with_group_lock, type: Boolean, desc: 'Prevent sharing a project with another group within this group'
optional :require_two_factor_authentication, type: Boolean, desc: 'Require all users in this group to setup Two-factor authentication'
optional :two_factor_grace_period, type: Integer, desc: 'Time before Two-factor authentication is enforced'
diff --git a/lib/api/helpers/packages/conan/api_helpers.rb b/lib/api/helpers/packages/conan/api_helpers.rb
index 994d3c4c473..a9d91895cfe 100644
--- a/lib/api/helpers/packages/conan/api_helpers.rb
+++ b/lib/api/helpers/packages/conan/api_helpers.rb
@@ -23,7 +23,7 @@ module API
end
def present_download_urls(entity)
- authorize!(:read_package, project)
+ authorize_read_package!(project)
presenter = ::Packages::Conan::PackagePresenter.new(
package,
@@ -161,7 +161,7 @@ module API
end
def download_package_file(file_type)
- authorize!(:read_package, project)
+ authorize_read_package!(project)
package_file = ::Packages::Conan::PackageFileFinder
.new(
@@ -173,7 +173,7 @@ module API
track_package_event('pull_package', :conan, category: 'API::ConanPackages', user: current_user, project: project, namespace: project.namespace) if params[:file_name] == ::Packages::Conan::FileMetadatum::PACKAGE_BINARY
- present_carrierwave_file!(package_file.file)
+ present_package_file!(package_file)
end
def find_or_create_package
diff --git a/lib/api/helpers/packages/dependency_proxy_helpers.rb b/lib/api/helpers/packages/dependency_proxy_helpers.rb
index b8ae1dddd7e..a09499e00d7 100644
--- a/lib/api/helpers/packages/dependency_proxy_helpers.rb
+++ b/lib/api/helpers/packages/dependency_proxy_helpers.rb
@@ -6,16 +6,18 @@ module API
module DependencyProxyHelpers
REGISTRY_BASE_URLS = {
npm: 'https://registry.npmjs.org/',
- pypi: 'https://pypi.org/simple/'
+ pypi: 'https://pypi.org/simple/',
+ maven: 'https://repo.maven.apache.org/maven2/'
}.freeze
APPLICATION_SETTING_NAMES = {
npm: 'npm_package_requests_forwarding',
- pypi: 'pypi_package_requests_forwarding'
+ pypi: 'pypi_package_requests_forwarding',
+ maven: 'maven_package_requests_forwarding'
}.freeze
def redirect_registry_request(forward_to_registry, package_type, options)
- if forward_to_registry && redirect_registry_request_available?(package_type)
+ if forward_to_registry && redirect_registry_request_available?(package_type) && maven_forwarding_ff_enabled?(package_type, options[:target])
::Gitlab::Tracking.event(self.options[:for].name, "#{package_type}_request_forward")
redirect(registry_url(package_type, options))
else
@@ -33,6 +35,8 @@ module API
"#{base_url}#{options[:package_name]}"
when :pypi
"#{base_url}#{options[:package_name]}/"
+ when :maven
+ "#{base_url}#{options[:path]}/#{options[:file_name]}"
end
end
@@ -46,6 +50,16 @@ module API
.attributes
.fetch(application_setting_name, false)
end
+
+ private
+
+ def maven_forwarding_ff_enabled?(package_type, target)
+ return true unless package_type == :maven
+ return true if Feature.enabled?(:maven_central_request_forwarding)
+ return false unless target
+
+ Feature.enabled?(:maven_central_request_forwarding, target.root_ancestor)
+ end
end
end
end
diff --git a/lib/api/helpers/packages_helpers.rb b/lib/api/helpers/packages_helpers.rb
index 2221eec0f82..687c8330cc8 100644
--- a/lib/api/helpers/packages_helpers.rb
+++ b/lib/api/helpers/packages_helpers.rb
@@ -14,7 +14,7 @@ module API
end
def authorize_read_package!(subject = user_project)
- authorize!(:read_package, subject)
+ authorize!(:read_package, subject.try(:packages_policy_subject) || subject)
end
def authorize_create_package!(subject = user_project)
@@ -53,6 +53,11 @@ module API
category = args.delete(:category) || self.options[:for].name
::Gitlab::Tracking.event(category, event_name.to_s, **args)
end
+
+ def present_package_file!(package_file, supports_direct_download: true)
+ package_file.package.touch_last_downloaded_at
+ present_carrierwave_file!(package_file.file, supports_direct_download: supports_direct_download)
+ end
end
end
end
diff --git a/lib/api/helpers/personal_access_tokens_helpers.rb b/lib/api/helpers/personal_access_tokens_helpers.rb
new file mode 100644
index 00000000000..db28daa5396
--- /dev/null
+++ b/lib/api/helpers/personal_access_tokens_helpers.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module API
+ module Helpers
+ module PersonalAccessTokensHelpers
+ def finder_params(current_user)
+ if current_user.can_admin_all_resources?
+ { user: user(params[:user_id]) }
+ else
+ { user: current_user, impersonation: false }
+ end
+ end
+
+ def user(user_id)
+ UserFinder.new(user_id).find_by_id
+ end
+
+ def restrict_non_admins!
+ return if params[:user_id].blank?
+
+ unauthorized! unless Ability.allowed?(current_user, :read_user_personal_access_tokens, user(params[:user_id]))
+ end
+
+ def find_token(id)
+ PersonalAccessToken.find(id) || not_found!
+ end
+
+ def revoke_token(token)
+ service = ::PersonalAccessTokens::RevokeService.new(current_user, token: token).execute
+
+ service.success? ? no_content! : bad_request!(nil)
+ end
+ end
+ end
+end
diff --git a/lib/api/helpers/projects_helpers.rb b/lib/api/helpers/projects_helpers.rb
index 628182ad1ab..7ca3f55b5a2 100644
--- a/lib/api/helpers/projects_helpers.rb
+++ b/lib/api/helpers/projects_helpers.rb
@@ -39,6 +39,7 @@ module API
optional :emails_disabled, type: Boolean, desc: 'Disable email notifications'
optional :show_default_award_emojis, type: Boolean, desc: 'Show default award emojis'
+ optional :show_diff_preview_in_email, type: Boolean, desc: 'Include the code diff preview in merge request notification emails'
optional :warn_about_potentially_unwanted_characters, type: Boolean, desc: 'Warn about Potentially Unwanted Characters'
optional :enforce_auth_checks_on_uploads, type: Boolean, desc: 'Enforce auth check on uploads'
optional :shared_runners_enabled, type: Boolean, desc: 'Flag indication if shared runners are enabled for that project'
@@ -57,8 +58,7 @@ module API
optional :only_allow_merge_if_all_discussions_are_resolved, type: Boolean, desc: 'Only allow to merge if all threads are resolved'
optional :tag_list, type: Array[String], coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce, desc: 'Deprecated: Use :topics instead'
optional :topics, type: Array[String], coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce, desc: 'The list of topics for a project'
- # TODO: remove rubocop disable - https://gitlab.com/gitlab-org/gitlab/issues/14960
- optional :avatar, type: File, desc: 'Avatar image for project' # rubocop:disable Scalability/FileUploads
+ optional :avatar, type: ::API::Validations::Types::WorkhorseFile, desc: 'Avatar image for project'
optional :printing_merge_request_link_enabled, type: Boolean, desc: 'Show link to create/view merge request when pushing from the command line'
optional :merge_method, type: String, values: %w(ff rebase_merge merge), desc: 'The merge method used when merging merge requests'
optional :suggestion_commit_message, type: String, desc: 'The commit message used to apply merge request suggestions'
@@ -160,6 +160,7 @@ module API
:request_access_enabled,
:resolve_outdated_diff_discussions,
:restrict_user_defined_variables,
+ :show_diff_preview_in_email,
:security_and_compliance_access_level,
:squash_option,
:shared_runners_enabled,
diff --git a/lib/api/helpers/resource_events_helpers.rb b/lib/api/helpers/resource_events_helpers.rb
new file mode 100644
index 00000000000..c47a58e8fce
--- /dev/null
+++ b/lib/api/helpers/resource_events_helpers.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module API
+ module Helpers
+ module ResourceEventsHelpers
+ def self.eventable_types
+ # This is a method instead of a constant, allowing EE to more easily extend it.
+ {
+ Issue => { feature_category: :team_planning, id_field: 'IID' },
+ MergeRequest => { feature_category: :code_review, id_field: 'IID' }
+ }
+ end
+ end
+ end
+end
+
+API::Helpers::ResourceEventsHelpers.prepend_mod_with('API::Helpers::ResourceEventsHelpers')
diff --git a/lib/api/helpers/resource_label_events_helpers.rb b/lib/api/helpers/resource_label_events_helpers.rb
deleted file mode 100644
index eeb68362c1d..00000000000
--- a/lib/api/helpers/resource_label_events_helpers.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-# frozen_string_literal: true
-
-module API
- module Helpers
- module ResourceLabelEventsHelpers
- def self.feature_category_per_eventable_type
- # This is a method instead of a constant, allowing EE to more easily
- # extend it.
- {
- Issue => :team_planning,
- MergeRequest => :code_review
- }
- end
- end
- end
-end
-
-API::Helpers::ResourceLabelEventsHelpers.prepend_mod_with('API::Helpers::ResourceLabelEventsHelpers')
diff --git a/lib/api/integrations/slack/events.rb b/lib/api/integrations/slack/events.rb
deleted file mode 100644
index 6227b75a9d7..00000000000
--- a/lib/api/integrations/slack/events.rb
+++ /dev/null
@@ -1,40 +0,0 @@
-# frozen_string_literal: true
-
-# This API endpoint handles all events sent from Slack once a Slack
-# workspace has installed the GitLab Slack app.
-#
-# See https://api.slack.com/apis/connections/events-api.
-module API
- class Integrations
- module Slack
- class Events < ::API::Base
- feature_category :integrations
-
- before { verify_slack_request! }
-
- helpers do
- def verify_slack_request!
- unauthorized! unless Request.verify!(request)
- end
- end
-
- namespace 'integrations/slack' do
- post :events do
- type = params['type']
- raise ArgumentError, "Unable to handle event type: '#{type}'" unless type == 'url_verification'
-
- status :ok
- UrlVerification.call(params)
- rescue ArgumentError => e
- # Track the error, but respond with a `2xx` because we don't want to risk
- # Slack rate-limiting, or disabling our app, due to error responses.
- # See https://api.slack.com/apis/connections/events-api.
- Gitlab::ErrorTracking.track_exception(e)
-
- no_content!
- end
- end
- end
- end
- end
-end
diff --git a/lib/api/integrations/slack/events/url_verification.rb b/lib/api/integrations/slack/events/url_verification.rb
deleted file mode 100644
index 4628b93665d..00000000000
--- a/lib/api/integrations/slack/events/url_verification.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-# frozen_string_literal: true
-
-module API
- class Integrations
- module Slack
- class Events
- class UrlVerification
- # When the GitLab Slack app is first configured to receive Slack events,
- # Slack will issue a special request to the endpoint and expect it to respond
- # with the `challenge` param.
- #
- # This must be done in-request, rather than on a queue.
- #
- # See https://api.slack.com/apis/connections/events-api.
- def self.call(params)
- { challenge: params[:challenge] }
- end
- end
- end
- end
- end
-end
diff --git a/lib/api/integrations/slack/request.rb b/lib/api/integrations/slack/request.rb
deleted file mode 100644
index df0109b07aa..00000000000
--- a/lib/api/integrations/slack/request.rb
+++ /dev/null
@@ -1,51 +0,0 @@
-# frozen_string_literal: true
-
-module API
- class Integrations
- module Slack
- module Request
- VERIFICATION_VERSION = 'v0'
- VERIFICATION_TIMESTAMP_HEADER = 'X-Slack-Request-Timestamp'
- VERIFICATION_SIGNATURE_HEADER = 'X-Slack-Signature'
- VERIFICATION_DELIMITER = ':'
- VERIFICATION_HMAC_ALGORITHM = 'sha256'
- VERIFICATION_TIMESTAMP_EXPIRY = 1.minute.to_i
-
- # Verify the request by comparing the given request signature in the header
- # with a signature value that we compute according to the steps in:
- # https://api.slack.com/authentication/verifying-requests-from-slack.
- def self.verify!(request)
- return false unless Gitlab::CurrentSettings.slack_app_signing_secret
-
- timestamp, signature = request.headers.values_at(
- VERIFICATION_TIMESTAMP_HEADER,
- VERIFICATION_SIGNATURE_HEADER
- )
-
- return false if timestamp.nil? || signature.nil?
- return false if Time.current.to_i - timestamp.to_i >= VERIFICATION_TIMESTAMP_EXPIRY
-
- request.body.rewind
-
- basestring = [
- VERIFICATION_VERSION,
- timestamp,
- request.body.read
- ].join(VERIFICATION_DELIMITER)
-
- hmac_digest = OpenSSL::HMAC.hexdigest(
- VERIFICATION_HMAC_ALGORITHM,
- Gitlab::CurrentSettings.slack_app_signing_secret,
- basestring
- )
-
- # Signature will look like: 'v0=a2114d57b48eac39b9ad189dd8316235a7b4a8d21a10bd27519666489c69b503'
- ActiveSupport::SecurityUtils.secure_compare(
- signature,
- "#{VERIFICATION_VERSION}=#{hmac_digest}"
- )
- end
- end
- end
- end
-end
diff --git a/lib/api/internal/base.rb b/lib/api/internal/base.rb
index 6f475fa8d74..c4464666020 100644
--- a/lib/api/internal/base.rb
+++ b/lib/api/internal/base.rb
@@ -133,11 +133,6 @@ module API
'Could not find a user for the given key' unless actor.user
end
- # TODO: backwards compatibility; remove after https://gitlab.com/gitlab-org/gitlab-shell/-/merge_requests/454 is merged
- def two_factor_otp_check
- { success: false, message: 'Feature is not available' }
- end
-
def two_factor_manual_otp_check
{ success: false, message: 'Feature is not available' }
end
@@ -339,13 +334,6 @@ module API
end
end
- # TODO: backwards compatibility; remove after https://gitlab.com/gitlab-org/gitlab-shell/-/merge_requests/454 is merged
- post '/two_factor_otp_check', feature_category: :authentication_and_authorization do
- status 200
-
- two_factor_manual_otp_check
- end
-
post '/two_factor_push_otp_check', feature_category: :authentication_and_authorization do
status 200
diff --git a/lib/api/maven_packages.rb b/lib/api/maven_packages.rb
index fb0221ee907..a3a25ec1696 100644
--- a/lib/api/maven_packages.rb
+++ b/lib/api/maven_packages.rb
@@ -22,6 +22,7 @@ module API
end
helpers ::API::Helpers::PackagesHelpers
+ helpers ::API::Helpers::Packages::DependencyProxyHelpers
helpers do
def path_exists?(path)
@@ -76,7 +77,10 @@ module API
format == 'jar'
end
- def present_carrierwave_file_with_head_support!(file, supports_direct_download: true)
+ def present_carrierwave_file_with_head_support!(package_file, supports_direct_download: true)
+ package_file.package.touch_last_downloaded_at
+ file = package_file.file
+
if head_request_on_aws_file?(file, supports_direct_download) && !file.file_storage?
return redirect(signed_head_url(file))
end
@@ -110,7 +114,31 @@ module API
project || group,
path: params[:path],
order_by_package_file: order_by_package_file
- ).execute!
+ ).execute
+ end
+
+ def find_and_present_package_file(package, file_name, format, params)
+ project = package&.project
+ package_file = nil
+
+ package_file = ::Packages::PackageFileFinder.new(package, file_name).execute if package
+
+ no_package_found = package_file ? false : true
+
+ redirect_registry_request(no_package_found, :maven, path: params[:path], file_name: params[:file_name], target: params[:target]) do
+ not_found!('Package') if no_package_found
+
+ case format
+ when 'md5'
+ package_file.file_md5
+ when 'sha1'
+ package_file.file_sha1
+ else
+ track_package_event('pull_package', :maven, project: project, namespace: project&.namespace) if jar_file?(format)
+
+ present_carrierwave_file_with_head_support!(package_file)
+ end
+ end
end
end
@@ -138,6 +166,8 @@ module API
package = fetch_package(file_name: file_name, project: project)
+ not_found!('Package') unless package
+
package_file = ::Packages::PackageFileFinder
.new(package, file_name).execute!
@@ -148,7 +178,7 @@ module API
package_file.file_sha1
else
track_package_event('pull_package', :maven, project: project, namespace: project.namespace) if jar_file?(format)
- present_carrierwave_file_with_head_support!(package_file.file)
+ present_carrierwave_file_with_head_support!(package_file)
end
end
@@ -166,31 +196,20 @@ module API
route_setting :authentication, job_token_allowed: true, deploy_token_allowed: true
get ':id/-/packages/maven/*path/:file_name', requirements: MAVEN_ENDPOINT_REQUIREMENTS do
# return a similar failure to group = find_group(params[:id])
- not_found!('Group') unless path_exists?(params[:path])
-
- file_name, format = extract_format(params[:file_name])
-
group = find_group(params[:id])
+ if Feature.disabled?(:maven_central_request_forwarding, group&.root_ancestor)
+ not_found!('Group') unless path_exists?(params[:path])
+ end
+
not_found!('Group') unless can?(current_user, :read_group, group)
+ file_name, format = extract_format(params[:file_name])
package = fetch_package(file_name: file_name, group: group)
- authorize_read_package!(package.project)
+ authorize_read_package!(package.project) if package
- package_file = ::Packages::PackageFileFinder
- .new(package, file_name).execute!
-
- case format
- when 'md5'
- package_file.file_md5
- when 'sha1'
- package_file.file_sha1
- else
- track_package_event('pull_package', :maven, project: package.project, namespace: package.project.namespace) if jar_file?(format)
-
- present_carrierwave_file_with_head_support!(package_file.file)
- end
+ find_and_present_package_file(package, file_name, format, params.merge(target: group))
end
end
@@ -208,7 +227,9 @@ module API
route_setting :authentication, job_token_allowed: true, deploy_token_allowed: true
get ':id/packages/maven/*path/:file_name', requirements: MAVEN_ENDPOINT_REQUIREMENTS do
# return a similar failure to user_project
- not_found!('Project') unless path_exists?(params[:path])
+ unless Feature.enabled?(:maven_central_request_forwarding, user_project&.root_ancestor)
+ not_found!('Project') unless path_exists?(params[:path])
+ end
authorize_read_package!(user_project)
@@ -216,19 +237,7 @@ module API
package = fetch_package(file_name: file_name, project: user_project)
- package_file = ::Packages::PackageFileFinder
- .new(package, file_name).execute!
-
- case format
- when 'md5'
- package_file.file_md5
- when 'sha1'
- package_file.file_sha1
- else
- track_package_event('pull_package', :maven, project: user_project, namespace: user_project.namespace) if jar_file?(format)
-
- present_carrierwave_file_with_head_support!(package_file.file)
- end
+ find_and_present_package_file(package, file_name, format, params.merge(target: user_project))
end
desc 'Workhorse authorize the maven package file upload' do
diff --git a/lib/api/members.rb b/lib/api/members.rb
index d26fdd09ee7..f4e38207aca 100644
--- a/lib/api/members.rb
+++ b/lib/api/members.rb
@@ -24,6 +24,7 @@ module API
params do
optional :query, type: String, desc: 'A query string to search for members'
optional :user_ids, type: Array[Integer], coerce_with: ::API::Validations::Types::CommaSeparatedToIntegerArray.coerce, desc: 'Array of user ids to look up for membership'
+ optional :skip_users, type: Array[Integer], coerce_with: ::API::Validations::Types::CommaSeparatedToIntegerArray.coerce, desc: 'Array of user ids to be skipped for membership'
optional :show_seat_info, type: Boolean, desc: 'Show seat information for members'
use :optional_filter_params_ee
use :pagination
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index a8f58e91067..1dc0e1f0d22 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -212,7 +212,17 @@ module API
recheck_mergeability_of(merge_requests: merge_requests) unless options[:skip_merge_status_recheck]
- present_cached merge_requests, expires_in: 8.hours, cache_context: -> (mr) { "#{current_user&.cache_key}:#{mr.merge_status}" }, **options
+ present_cached merge_requests,
+ expires_in: 8.hours,
+ cache_context: -> (mr) do
+ [
+ current_user&.cache_key,
+ mr.merge_status,
+ mr.merge_request_assignees.map(&:cache_key),
+ mr.merge_request_reviewers.map(&:cache_key)
+ ].join(":")
+ end,
+ **options
end
desc 'Create a merge request' do
@@ -544,6 +554,19 @@ module API
render_api_error!(e.message, 409)
end
+ desc 'Remove merge request approvals' do
+ detail 'This feature was added in GitLab 15.4'
+ end
+ put ':id/merge_requests/:merge_request_iid/reset_approvals', feature_category: :code_review, urgency: :low do
+ merge_request = find_project_merge_request(params[:merge_request_iid])
+
+ unauthorized! unless current_user.bot? && merge_request.can_be_approved_by?(current_user)
+
+ merge_request.approvals.delete_all
+
+ status :accepted
+ end
+
desc 'List issues that will be closed on merge' do
success Entities::MRNote
end
diff --git a/lib/api/ml/mlflow.rb b/lib/api/ml/mlflow.rb
new file mode 100644
index 00000000000..4f5bd42f8f9
--- /dev/null
+++ b/lib/api/ml/mlflow.rb
@@ -0,0 +1,171 @@
+# frozen_string_literal: true
+
+require 'mime/types'
+
+module API
+ # MLFlow integration API, replicating the Rest API https://www.mlflow.org/docs/latest/rest-api.html#rest-api
+ module Ml
+ class Mlflow < ::API::Base
+ include APIGuard
+
+ # The first part of the url is the namespace, the second part of the URL is what the MLFlow client calls
+ MLFLOW_API_PREFIX = ':id/ml/mflow/api/2.0/mlflow/'
+
+ allow_access_with_scope :api
+ allow_access_with_scope :read_api, if: -> (request) { request.get? || request.head? }
+
+ before do
+ authenticate!
+ not_found! unless Feature.enabled?(:ml_experiment_tracking, user_project)
+ end
+
+ feature_category :mlops
+
+ content_type :json, 'application/json'
+ default_format :json
+
+ helpers do
+ def resource_not_found!
+ render_structured_api_error!({ error_code: 'RESOURCE_DOES_NOT_EXIST' }, 404)
+ end
+
+ def resource_already_exists!
+ render_structured_api_error!({ error_code: 'RESOURCE_ALREADY_EXISTS' }, 400)
+ end
+ end
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
+ desc 'API to interface with MLFlow Client, REST API version 1.28.0' do
+ detail 'This feature is gated by :ml_experiment_tracking.'
+ end
+ namespace MLFLOW_API_PREFIX do
+ resource :experiments do
+ desc 'Fetch experiment by experiment_id' do
+ success Entities::Ml::Mlflow::Experiment
+ detail 'https://www.mlflow.org/docs/1.28.0/rest-api.html#get-experiment'
+ end
+ params do
+ optional :experiment_id, type: String, default: '', desc: 'Experiment ID, in reference to the project'
+ end
+ get 'get', urgency: :low do
+ experiment = ::Ml::Experiment.by_project_id_and_iid(user_project.id, params[:experiment_id])
+
+ resource_not_found! unless experiment
+
+ present experiment, with: Entities::Ml::Mlflow::Experiment
+ end
+
+ desc 'Fetch experiment by experiment_name' do
+ success Entities::Ml::Mlflow::Experiment
+ detail 'https://www.mlflow.org/docs/1.28.0/rest-api.html#get-experiment-by-name'
+ end
+ params do
+ optional :experiment_name, type: String, default: '', desc: 'Experiment name'
+ end
+ get 'get-by-name', urgency: :low do
+ experiment = ::Ml::Experiment.by_project_id_and_name(user_project, params[:experiment_name])
+
+ resource_not_found! unless experiment
+
+ present experiment, with: Entities::Ml::Mlflow::Experiment
+ end
+
+ desc 'Create experiment' do
+ success Entities::Ml::Mlflow::NewExperiment
+ detail 'https://www.mlflow.org/docs/1.28.0/rest-api.html#create-experiment'
+ end
+ params do
+ requires :name, type: String, desc: 'Experiment name'
+ optional :artifact_location, type: String, desc: 'This will be ignored'
+ optional :tags, type: Array, desc: 'This will be ignored'
+ end
+ post 'create', urgency: :low do
+ resource_already_exists! if ::Ml::Experiment.has_record?(user_project.id, params[:name])
+
+ experiment = ::Ml::Experiment.create!(name: params[:name],
+ user: current_user,
+ project: user_project)
+
+ present experiment, with: Entities::Ml::Mlflow::NewExperiment
+ end
+ end
+
+ resource :runs do
+ desc 'Gets an MLFlow Run, which maps to GitLab Candidates' do
+ success Entities::Ml::Mlflow::Run
+ detail 'https://www.mlflow.org/docs/1.28.0/rest-api.html#get-run'
+ end
+ params do
+ optional :run_id, type: String, desc: 'UUID of the candidate.'
+ optional :run_uuid, type: String, desc: 'This parameter is ignored'
+ end
+ get 'get', urgency: :low do
+ candidate = ::Ml::Candidate.with_project_id_and_iid(user_project.id, params[:run_id])
+
+ resource_not_found! unless candidate
+
+ present candidate, with: Entities::Ml::Mlflow::Run
+ end
+
+ desc 'Creates a Run.' do
+ success Entities::Ml::Mlflow::Run
+ detail ['https://www.mlflow.org/docs/1.28.0/rest-api.html#create-run',
+ 'MLFlow Runs map to GitLab Candidates']
+ end
+ params do
+ requires :experiment_id, type: Integer,
+ desc: 'Id for the experiment, relative to the project'
+ optional :start_time, type: Integer,
+ desc: 'Unix timestamp in milliseconds of when the run started.',
+ default: 0
+ optional :user_id, type: String, desc: 'This will be ignored'
+ optional :tags, type: Array, desc: 'This will be ignored'
+ end
+ post 'create', urgency: :low do
+ experiment = ::Ml::Experiment.by_project_id_and_iid(user_project.id, params[:experiment_id].to_i)
+
+ resource_not_found! unless experiment
+
+ candidate = ::Ml::Candidate.create!(
+ experiment: experiment,
+ user: current_user,
+ start_time: params[:start_time] || 0
+ )
+
+ present candidate, with: Entities::Ml::Mlflow::Run
+ end
+
+ desc 'Updates a Run.' do
+ success Entities::Ml::Mlflow::UpdateRun
+ detail ['https://www.mlflow.org/docs/1.28.0/rest-api.html#update-run',
+ 'MLFlow Runs map to GitLab Candidates']
+ end
+ params do
+ optional :run_id, type: String, desc: 'UUID of the candidate.'
+ optional :status, type: String,
+ values: ::Ml::Candidate.statuses.keys.map(&:upcase),
+ desc: "Status of the run. Accepts: " \
+ "#{::Ml::Candidate.statuses.keys.map(&:upcase)}."
+ optional :end_time, type: Integer, desc: 'Ending time of the run'
+ end
+ post 'update', urgency: :low do
+ candidate = ::Ml::Candidate.with_project_id_and_iid(user_project.id, params[:run_id])
+
+ resource_not_found! unless candidate
+
+ candidate.status = params[:status].downcase if params[:status]
+ candidate.end_time = params[:end_time] if params[:end_time]
+
+ candidate.save if candidate.valid?
+
+ present candidate, with: Entities::Ml::Mlflow::UpdateRun
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/namespaces.rb b/lib/api/namespaces.rb
index a12fbbb9bb6..eeb66c86b3b 100644
--- a/lib/api/namespaces.rb
+++ b/lib/api/namespaces.rb
@@ -66,6 +66,8 @@ module API
optional :parent_id, type: Integer, desc: "The ID of the parent namespace. If no ID is specified, only top-level namespaces are considered."
end
get ':namespace/exists', requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS, feature_category: :subgroups, urgency: :low do
+ check_rate_limit!(:namespace_exists, scope: current_user)
+
namespace_path = params[:namespace]
existing_namespaces_within_the_parent = Namespace.without_project_namespaces.by_parent(params[:parent_id])
diff --git a/lib/api/npm_project_packages.rb b/lib/api/npm_project_packages.rb
index 21bb2e69799..166c0b755fe 100644
--- a/lib/api/npm_project_packages.rb
+++ b/lib/api/npm_project_packages.rb
@@ -35,7 +35,7 @@ module API
track_package_event('pull_package', package, category: 'API::NpmPackages', project: project, namespace: project.namespace)
- present_carrierwave_file!(package_file.file)
+ present_package_file!(package_file)
end
desc 'Create NPM package' do
diff --git a/lib/api/nuget_project_packages.rb b/lib/api/nuget_project_packages.rb
index 1e630cffea1..3e05ea13311 100644
--- a/lib/api/nuget_project_packages.rb
+++ b/lib/api/nuget_project_packages.rb
@@ -193,7 +193,7 @@ module API
)
# nuget and dotnet don't support 302 Moved status codes, supports_direct_download has to be set to false
- present_carrierwave_file!(package_file.file, supports_direct_download: false)
+ present_package_file!(package_file, supports_direct_download: false)
end
end
end
diff --git a/lib/api/personal_access_tokens.rb b/lib/api/personal_access_tokens.rb
index 0d7d2dc6a0c..1c00569bba2 100644
--- a/lib/api/personal_access_tokens.rb
+++ b/lib/api/personal_access_tokens.rb
@@ -18,34 +18,10 @@ module API
before do
authenticate!
- restrict_non_admins! unless current_user.admin?
+ restrict_non_admins! unless current_user.can_admin_all_resources?
end
- helpers do
- def finder_params(current_user)
- current_user.admin? ? { user: user(params[:user_id]) } : { user: current_user, impersonation: false }
- end
-
- def user(user_id)
- UserFinder.new(user_id).find_by_id
- end
-
- def restrict_non_admins!
- return if params[:user_id].blank?
-
- unauthorized! unless Ability.allowed?(current_user, :read_user_personal_access_tokens, user(params[:user_id]))
- end
-
- def find_token(id)
- PersonalAccessToken.find(id) || not_found!
- end
-
- def revoke_token(token)
- service = ::PersonalAccessTokens::RevokeService.new(current_user, token: token).execute
-
- service.success? ? no_content! : bad_request!(nil)
- end
- end
+ helpers ::API::Helpers::PersonalAccessTokensHelpers
resources :personal_access_tokens do
get do
@@ -63,14 +39,10 @@ module API
present token, with: Entities::PersonalAccessToken
else
# Only admins should be informed if the token doesn't exist
- current_user.admin? ? not_found! : unauthorized!
+ current_user.can_admin_all_resources? ? not_found! : unauthorized!
end
end
- delete 'self' do
- revoke_token(access_token)
- end
-
delete ':id' do
token = find_token(params[:id])
diff --git a/lib/api/personal_access_tokens/self_revocation.rb b/lib/api/personal_access_tokens/self_revocation.rb
new file mode 100644
index 00000000000..22e07f4cc7b
--- /dev/null
+++ b/lib/api/personal_access_tokens/self_revocation.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module API
+ class PersonalAccessTokens
+ class SelfRevocation < ::API::Base
+ include APIGuard
+
+ feature_category :authentication_and_authorization
+
+ helpers ::API::Helpers::PersonalAccessTokensHelpers
+
+ # As any token regardless of `scope` should be able to revoke itself
+ # all availabe scopes are allowed for this API class.
+ # Please be aware of the permissive scope when adding new endpoints to this class.
+ allow_access_with_scope(Gitlab::Auth.all_available_scopes)
+
+ before { authenticate! }
+
+ resource :personal_access_tokens do
+ delete 'self' do
+ revoke_token(access_token)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index 6ed480518ee..8c58cc585d8 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -453,6 +453,8 @@ module API
filter_attributes_using_license!(attrs)
verify_update_project_attrs!(user_project, attrs)
+ user_project.remove_avatar! if attrs.key?(:avatar) && attrs[:avatar].nil?
+
result = ::Projects::UpdateService.new(user_project, current_user, attrs).execute
if result[:status] == :success
@@ -743,6 +745,22 @@ module API
end
end
+ desc 'Get the namespaces to where the project can be transferred'
+ params do
+ optional :search, type: String, desc: 'Return list of namespaces matching the search criteria'
+ use :pagination
+ end
+ get ":id/transfer_locations", feature_category: :projects do
+ authorize! :change_namespace, user_project
+ args = declared_params(include_missing: false)
+ args[:permission_scope] = :transfer_projects
+
+ groups = ::Groups::UserGroupsFinder.new(current_user, current_user, args).execute
+ groups = groups.with_route
+
+ present_groups(groups)
+ end
+
desc 'Show the storage information' do
success Entities::ProjectRepositoryStorage
end
diff --git a/lib/api/pypi_packages.rb b/lib/api/pypi_packages.rb
index f8a7a3c0ecc..ae583ca968a 100644
--- a/lib/api/pypi_packages.rb
+++ b/lib/api/pypi_packages.rb
@@ -120,7 +120,7 @@ module API
track_package_event('pull_package', :pypi)
- present_carrierwave_file!(package_file.file, supports_direct_download: true)
+ present_package_file!(package_file, supports_direct_download: true)
end
desc 'The PyPi Simple Group Index Endpoint' do
@@ -180,7 +180,7 @@ module API
track_package_event('pull_package', :pypi, project: project, namespace: project.namespace)
- present_carrierwave_file!(package_file.file, supports_direct_download: true)
+ present_package_file!(package_file, supports_direct_download: true)
end
desc 'The PyPi Simple Project Index Endpoint' do
diff --git a/lib/api/releases.rb b/lib/api/releases.rb
index 10e879ec70b..cdfcce9dddb 100644
--- a/lib/api/releases.rb
+++ b/lib/api/releases.rb
@@ -100,6 +100,62 @@ module API
present release, with: Entities::Release, current_user: current_user, include_html_description: params[:include_html_description]
end
+ desc 'Download a project release asset file' do
+ detail 'This feature was introduced in GitLab 15.4.'
+ named 'download_release_asset_file'
+ end
+ params do
+ requires :tag_name, type: String,
+ desc: 'The name of the tag.', as: :tag
+ requires :file_path, type: String,
+ file_path: true,
+ desc: 'The path to the file to download, as specified when creating the release asset.'
+ end
+ route_setting :authentication, job_token_allowed: true
+ get ':id/releases/:tag_name/downloads/*file_path', format: false, requirements: RELEASE_ENDPOINT_REQUIREMENTS do
+ authorize_download_code!
+
+ not_found! unless release
+
+ link = release.links.find_by_filepath!("/#{params[:file_path]}")
+
+ not_found! unless link
+
+ redirect link.url
+ end
+
+ desc 'Get the latest project release' do
+ detail 'This feature was introduced in GitLab 15.4.'
+ named 'get_latest_release'
+ end
+ params do
+ requires :suffix_path, type: String, file_path: true, desc: 'The path to be suffixed to the latest release'
+ end
+ route_setting :authentication, job_token_allowed: true
+ get ':id/releases/permalink/latest(/)(*suffix_path)', format: false, requirements: RELEASE_ENDPOINT_REQUIREMENTS do
+ authorize_download_code!
+
+ # Try to find the latest release
+ latest_release = find_latest_release
+ not_found! unless latest_release
+
+ # Build the full API URL with the tag of the latest release
+ redirect_url = api_v4_projects_releases_path(id: user_project.id, tag_name: latest_release.tag)
+
+ # Include the additional suffix_path if present
+ redirect_url += "/#{params[:suffix_path]}" if params[:suffix_path].present?
+
+ # Include any query parameter except `order_by` since we have plans to extend it in the future.
+ # See https://gitlab.com/gitlab-org/gitlab/-/issues/352945 for reference.
+ query_parameters_except_order_by = get_query_params.except('order_by')
+
+ if query_parameters_except_order_by.present?
+ redirect_url += "?#{query_parameters_except_order_by.compact.to_param}"
+ end
+
+ redirect redirect_url
+ end
+
desc 'Create a new release' do
detail 'This feature was introduced in GitLab 11.7.'
named 'create_release'
@@ -232,6 +288,16 @@ module API
@release ||= user_project.releases.find_by_tag(params[:tag])
end
+ def find_latest_release
+ ReleasesFinder.new(user_project, current_user, { order_by: 'released_at', sort: 'desc' }).execute.first
+ end
+
+ def get_query_params
+ return {} unless @request.query_string.present?
+
+ Rack::Utils.parse_nested_query(@request.query_string)
+ end
+
def log_release_created_audit_event(release)
# extended in EE
end
diff --git a/lib/api/resource_label_events.rb b/lib/api/resource_label_events.rb
index cd56809f45a..e74b6509a17 100644
--- a/lib/api/resource_label_events.rb
+++ b/lib/api/resource_label_events.rb
@@ -7,20 +7,22 @@ module API
before { authenticate! }
- Helpers::ResourceLabelEventsHelpers.feature_category_per_eventable_type.each do |eventable_type, feature_category|
+ Helpers::ResourceEventsHelpers.eventable_types.each do |eventable_type, details|
parent_type = eventable_type.parent_class.to_s.underscore
eventables_str = eventable_type.to_s.underscore.pluralize
+ human_eventable_str = eventable_type.to_s.underscore.humanize.downcase
+ feature_category = details[:feature_category]
params do
requires :id, type: String, desc: "The ID of a #{parent_type}"
end
resource parent_type.pluralize.to_sym, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
- desc "Get a list of #{eventable_type.to_s.downcase} resource label events" do
+ desc "Get a list of #{human_eventable_str} resource label events" do
success Entities::ResourceLabelEvent
detail 'This feature was introduced in 11.3'
end
params do
- requires :eventable_id, types: [Integer, String], desc: 'The ID of the eventable'
+ requires :eventable_id, types: [Integer, String], desc: "The #{details[:id_field]} of the #{human_eventable_str}"
use :pagination
end
@@ -32,13 +34,13 @@ module API
present ResourceLabelEvent.visible_to_user?(current_user, paginate(events)), with: Entities::ResourceLabelEvent
end
- desc "Get a single #{eventable_type.to_s.downcase} resource label event" do
+ desc "Get a single #{human_eventable_str} resource label event" do
success Entities::ResourceLabelEvent
detail 'This feature was introduced in 11.3'
end
params do
requires :event_id, type: String, desc: 'The ID of a resource label event'
- requires :eventable_id, types: [Integer, String], desc: 'The ID of the eventable'
+ requires :eventable_id, types: [Integer, String], desc: "The #{details[:id_field]} of the #{human_eventable_str}"
end
get ":id/#{eventables_str}/:eventable_id/resource_label_events/:event_id", feature_category: feature_category do
eventable = find_noteable(eventable_type, params[:eventable_id])
diff --git a/lib/api/resource_state_events.rb b/lib/api/resource_state_events.rb
index 4b92f320d6f..f817d55c505 100644
--- a/lib/api/resource_state_events.rb
+++ b/lib/api/resource_state_events.rb
@@ -7,41 +7,41 @@ module API
before { authenticate! }
- {
- Issue => :team_planning,
- MergeRequest => :code_review
- }.each do |eventable_class, feature_category|
- eventable_name = eventable_class.to_s.underscore
+ Helpers::ResourceEventsHelpers.eventable_types.each do |eventable_type, details|
+ parent_type = eventable_type.parent_class.to_s.underscore
+ eventables_str = eventable_type.to_s.underscore.pluralize
+ human_eventable_str = eventable_type.to_s.underscore.humanize.downcase
+ feature_category = details[:feature_category]
params do
- requires :id, type: String, desc: "The ID of a project"
+ requires :id, type: String, desc: "The ID of a #{parent_type}"
end
- resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
- desc "Get a list of #{eventable_class.to_s.downcase} resource state events" do
+ resource parent_type.pluralize.to_sym, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
+ desc "Get a list of #{human_eventable_str} resource state events" do
success Entities::ResourceStateEvent
end
params do
- requires :eventable_iid, types: Integer, desc: "The IID of the #{eventable_name}"
+ requires :eventable_id, types: Integer, desc: "The #{details[:id_field]} of the #{human_eventable_str}"
use :pagination
end
- get ":id/#{eventable_name.pluralize}/:eventable_iid/resource_state_events", feature_category: feature_category, urgency: :low do
- eventable = find_noteable(eventable_class, params[:eventable_iid])
+ get ":id/#{eventables_str}/:eventable_id/resource_state_events", feature_category: feature_category, urgency: :low do
+ eventable = find_noteable(eventable_type, params[:eventable_id])
events = ResourceStateEventFinder.new(current_user, eventable).execute
present paginate(events), with: Entities::ResourceStateEvent
end
- desc "Get a single #{eventable_class.to_s.downcase} resource state event" do
+ desc "Get a single #{human_eventable_str} resource state event" do
success Entities::ResourceStateEvent
end
params do
- requires :eventable_iid, types: Integer, desc: "The IID of the #{eventable_name}"
+ requires :eventable_id, types: Integer, desc: "The #{details[:id_field]} of the #{human_eventable_str}"
requires :event_id, type: Integer, desc: 'The ID of a resource state event'
end
- get ":id/#{eventable_name.pluralize}/:eventable_iid/resource_state_events/:event_id", feature_category: feature_category do
- eventable = find_noteable(eventable_class, params[:eventable_iid])
+ get ":id/#{eventables_str}/:eventable_id/resource_state_events/:event_id", feature_category: feature_category do
+ eventable = find_noteable(eventable_type, params[:eventable_id])
event = ResourceStateEventFinder.new(current_user, eventable).find(params[:event_id])
diff --git a/lib/api/rpm_project_packages.rb b/lib/api/rpm_project_packages.rb
new file mode 100644
index 00000000000..d17470ae92d
--- /dev/null
+++ b/lib/api/rpm_project_packages.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+module API
+ class RpmProjectPackages < ::API::Base
+ helpers ::API::Helpers::PackagesHelpers
+ helpers ::API::Helpers::Packages::BasicAuthHelpers
+ include ::API::Helpers::Authentication
+
+ feature_category :package_registry
+
+ before do
+ require_packages_enabled!
+
+ not_found! unless ::Feature.enabled?(:rpm_packages, authorized_user_project)
+
+ authorize_read_package!(authorized_user_project)
+ end
+
+ authenticate_with do |accept|
+ accept.token_types(:personal_access_token_with_username, :deploy_token_with_username, :job_token_with_username)
+ .sent_through(:http_basic_auth)
+ end
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
+ namespace ':id/packages/rpm' do
+ desc 'Download repository metadata files'
+ params do
+ requires :file_name, type: String, desc: 'Repository metadata file name'
+ end
+ get 'repodata/*file_name', requirements: { file_name: API::NO_SLASH_URL_PART_REGEX } do
+ not_found!
+ end
+
+ desc 'Download RPM package files'
+ params do
+ requires :package_file_id, type: Integer, desc: 'RPM package file id'
+ requires :file_name, type: String, desc: 'RPM package file name'
+ end
+ get '*package_file_id/*file_name', requirements: { file_name: API::NO_SLASH_URL_PART_REGEX } do
+ not_found!
+ end
+
+ desc 'Upload a RPM package'
+ post do
+ authorize_create_package!(authorized_user_project)
+
+ if authorized_user_project.actual_limits.exceeded?(:rpm_max_file_size, params[:file].size)
+ bad_request!('File is too large')
+ end
+
+ not_found!
+ end
+
+ desc 'Authorize package upload from workhorse'
+ post 'authorize' do
+ not_found!
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/rubygem_packages.rb b/lib/api/rubygem_packages.rb
index 85bbd0879b7..b4d02613e4c 100644
--- a/lib/api/rubygem_packages.rb
+++ b/lib/api/rubygem_packages.rb
@@ -65,7 +65,7 @@ module API
requires :file_name, type: String, desc: 'Package file name'
end
get "gems/:file_name", requirements: FILE_NAME_REQUIREMENTS do
- authorize!(:read_package, user_project)
+ authorize_read_package!(user_project)
package_files = ::Packages::PackageFile
.for_rubygem_with_file_name(user_project, params[:file_name])
@@ -74,7 +74,7 @@ module API
track_package_event('pull_package', :rubygems, project: user_project, namespace: user_project.namespace)
- present_carrierwave_file!(package_file.file)
+ present_package_file!(package_file)
end
namespace 'api/v1' do
diff --git a/lib/api/search.rb b/lib/api/search.rb
index 7aa3cf8a5cb..44bb4228786 100644
--- a/lib/api/search.rb
+++ b/lib/api/search.rb
@@ -65,9 +65,26 @@ module API
set_global_search_log_information
+ Gitlab::Metrics::GlobalSearchSlis.record_apdex(
+ elapsed: @search_duration_s,
+ search_type: search_type,
+ search_level: search_service.level,
+ search_scope: search_scope
+ )
+
Gitlab::UsageDataCounters::SearchCounter.count(:all_searches)
paginate(@results)
+
+ ensure
+ # If we raise an error somewhere in the @search_duration_s benchmark block, we will end up here
+ # with a 200 status code, but an empty @search_duration_s.
+ Gitlab::Metrics::GlobalSearchSlis.record_error_rate(
+ error: @search_duration_s.nil? || (status < 200 || status >= 400),
+ search_type: search_type,
+ search_level: search_service(additional_params).level,
+ search_scope: search_scope
+ )
end
def snippets?
diff --git a/lib/api/settings.rb b/lib/api/settings.rb
index c25a56d5f08..f393f862f55 100644
--- a/lib/api/settings.rb
+++ b/lib/api/settings.rb
@@ -98,6 +98,7 @@ module API
optional :max_export_size, type: Integer, desc: 'Maximum export size in MB'
optional :max_import_size, type: Integer, desc: 'Maximum import size in MB'
optional :max_pages_size, type: Integer, desc: 'Maximum size of pages in MB'
+ optional :max_pages_custom_domains_per_project, type: Integer, desc: 'Maximum number of GitLab Pages custom domains per project'
optional :metrics_method_call_threshold, type: Integer, desc: 'A method call is only tracked when it takes longer to complete than the given amount of milliseconds.'
optional :password_authentication_enabled, type: Boolean, desc: 'Flag indicating if password authentication is enabled for the web interface' # support legacy names, can be removed in v5
optional :password_authentication_enabled_for_web, type: Boolean, desc: 'Flag indicating if password authentication is enabled for the web interface'
diff --git a/lib/api/tags.rb b/lib/api/tags.rb
index 97a2aebf53b..c8ac68189f5 100644
--- a/lib/api/tags.rb
+++ b/lib/api/tags.rb
@@ -22,8 +22,8 @@ module API
params do
optional :sort, type: String, values: %w[asc desc], default: 'desc',
desc: 'Return tags sorted in updated by `asc` or `desc` order.'
- optional :order_by, type: String, values: %w[name updated], default: 'updated',
- desc: 'Return tags ordered by `name` or `updated` fields.'
+ optional :order_by, type: String, values: %w[name updated version], default: 'updated',
+ desc: 'Return tags ordered by `name`, `updated`, `version` fields.'
optional :search, type: String, desc: 'Return list of tags matching the search criteria'
optional :page_token, type: String, desc: 'Name of tag to start the paginaition from'
use :pagination
diff --git a/lib/api/topics.rb b/lib/api/topics.rb
index a08b4c6c107..38cfdc44021 100644
--- a/lib/api/topics.rb
+++ b/lib/api/topics.rb
@@ -94,5 +94,25 @@ module API
destroy_conditionally!(topic)
end
+
+ desc 'Merge topics' do
+ detail 'This feature was introduced in GitLab 15.4.'
+ success Entities::Projects::Topic
+ end
+ params do
+ requires :source_topic_id, type: Integer, desc: 'ID of source project topic'
+ requires :target_topic_id, type: Integer, desc: 'ID of target project topic'
+ end
+ post 'topics/merge' do
+ authenticated_as_admin!
+
+ source_topic = ::Projects::Topic.find(params[:source_topic_id])
+ target_topic = ::Projects::Topic.find(params[:target_topic_id])
+
+ response = ::Topics::MergeService.new(source_topic, target_topic).execute
+ render_api_error!(response.message, :bad_request) if response.error?
+
+ present target_topic, with: Entities::Projects::Topic
+ end
end
end
diff --git a/lib/api/users.rb b/lib/api/users.rb
index c93c0f601a0..1d1c633824e 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -54,8 +54,7 @@ module API
optional :admin, type: Boolean, desc: 'Flag indicating the user is an administrator'
optional :can_create_group, type: Boolean, desc: 'Flag indicating the user can create groups'
optional :external, type: Boolean, desc: 'Flag indicating the user is an external user'
- # TODO: remove rubocop disable - https://gitlab.com/gitlab-org/gitlab/issues/14960
- optional :avatar, type: File, desc: 'Avatar image for user' # rubocop:disable Scalability/FileUploads
+ optional :avatar, type: ::API::Validations::Types::WorkhorseFile, desc: 'Avatar image for user'
optional :theme_id, type: Integer, desc: 'The GitLab theme for the user'
optional :color_scheme_id, type: Integer, desc: 'The color scheme for the file viewer'
optional :private_profile, type: Boolean, desc: 'Flag indicating the user has a private profile'
@@ -733,7 +732,7 @@ module API
unless user.can_be_deactivated?
forbidden!('A blocked user cannot be deactivated by the API') if user.blocked?
forbidden!('An internal user cannot be deactivated by the API') if user.internal?
- forbidden!("The user you are trying to deactivate has been active in the past #{::User::MINIMUM_INACTIVE_DAYS} days and cannot be deactivated")
+ forbidden!("The user you are trying to deactivate has been active in the past #{Gitlab::CurrentSettings.deactivate_dormant_users_period} days and cannot be deactivated")
end
if user.deactivate
diff --git a/lib/banzai/filter/blockquote_fence_filter.rb b/lib/banzai/filter/blockquote_fence_filter.rb
index e07cbfe8d85..e5cf20d00df 100644
--- a/lib/banzai/filter/blockquote_fence_filter.rb
+++ b/lib/banzai/filter/blockquote_fence_filter.rb
@@ -6,13 +6,13 @@ module Banzai
REGEX = %r{
#{::Gitlab::Regex.markdown_code_or_html_blocks}
|
- (?=^>>>\ *\n.*\n>>>\ *$)(?:
+ (?=(?<=^\n|\A)>>>\ *\n.*\n>>>\ *(?=\n$|\z))(?:
# Blockquote:
# >>>
# Anything, including code and HTML blocks
# >>>
- ^>>>\ *\n
+ (?<=^\n|\A)>>>\ *\n
(?<quote>
(?:
# Any character that doesn't introduce a code or HTML block
@@ -30,7 +30,7 @@ module Banzai
\g<html>
)+?
)
- \n>>>\ *$
+ \n>>>\ *(?=\n$|\z)
)
}mx.freeze
diff --git a/lib/banzai/filter/kroki_filter.rb b/lib/banzai/filter/kroki_filter.rb
index 845c7f2bc0a..713ff2439fc 100644
--- a/lib/banzai/filter/kroki_filter.rb
+++ b/lib/banzai/filter/kroki_filter.rb
@@ -14,7 +14,10 @@ module Banzai
return doc unless settings.kroki_enabled
diagram_selectors = ::Gitlab::Kroki.formats(settings)
- .map { |diagram_type| %(pre[lang="#{diagram_type}"] > code) }
+ .map do |diagram_type|
+ %(pre[lang="#{diagram_type}"] > code,
+ pre > code[lang="#{diagram_type}"])
+ end
.join(', ')
xpath = Gitlab::Utils::Nokogiri.css_to_xpath(diagram_selectors)
@@ -22,7 +25,7 @@ module Banzai
diagram_format = "svg"
doc.xpath(xpath).each do |node|
- diagram_type = node.parent['lang']
+ diagram_type = node.parent['lang'] || node['lang']
diagram_src = node.content
image_src = create_image_src(diagram_type, diagram_format, diagram_src)
img_tag = Nokogiri::HTML::DocumentFragment.parse(%(<img src="#{image_src}" />))
@@ -33,8 +36,8 @@ module Banzai
img_tag.set_attribute('hidden', '') if lazy_load
img_tag.set_attribute('class', 'js-render-kroki')
- img_tag.set_attribute('data-diagram', node.parent['lang'])
- img_tag.set_attribute('data-diagram-src', "data:text/plain;base64,#{Base64.strict_encode64(node.content)}")
+ img_tag.set_attribute('data-diagram', diagram_type)
+ img_tag.set_attribute('data-diagram-src', "data:text/plain;base64,#{Base64.strict_encode64(diagram_src)}")
node.parent.replace(img_tag)
end
diff --git a/lib/banzai/filter/math_filter.rb b/lib/banzai/filter/math_filter.rb
index 0ac506776be..1ca4b2c89db 100644
--- a/lib/banzai/filter/math_filter.rb
+++ b/lib/banzai/filter/math_filter.rb
@@ -7,7 +7,7 @@ require 'uri'
# - app/assets/javascripts/behaviors/markdown/nodes/code_block.js
module Banzai
module Filter
- # HTML filter that adds class="code math" and removes the dollar sign in $`2+2`$.
+ # HTML filter that implements our math syntax, adding class="code math"
#
class MathFilter < HTML::Pipeline::Filter
CSS_MATH = 'pre.code.language-math'
@@ -15,14 +15,42 @@ module Banzai
CSS_CODE = 'code'
XPATH_CODE = Gitlab::Utils::Nokogiri.css_to_xpath(CSS_CODE).freeze
+ # These are based on the Pandoc heuristics,
+ # https://pandoc.org/MANUAL.html#extension-tex_math_dollars
+ # Note: at this time, using a dollar sign literal, `\$` inside
+ # a math statement does not work correctly.
+ # Corresponds to the "$...$" syntax
+ DOLLAR_INLINE_PATTERN = %r{
+ (?<matched>\$(?<math>(?:\S[^$\n]*?\S|[^$\s]))\$)(?:[^\d]|$)
+ }x.freeze
+
+ # Corresponds to the "$$...$$" syntax
+ DOLLAR_DISPLAY_INLINE_PATTERN = %r{
+ (?<matched>\$\$\ *(?<math>[^$\n]+?)\ *\$\$)
+ }x.freeze
+
+ # Corresponds to the $$\n...\n$$ syntax
+ DOLLAR_DISPLAY_BLOCK_PATTERN = %r{
+ ^(?<matched>\$\$\ *\n(?<math>.*)\n\$\$\ *)$
+ }x.freeze
+
+ # Order dependent. Handle the `$$` syntax before the `$` syntax
+ DOLLAR_MATH_PIPELINE = [
+ { pattern: DOLLAR_DISPLAY_INLINE_PATTERN, tag: :code, style: :display },
+ { pattern: DOLLAR_DISPLAY_BLOCK_PATTERN, tag: :pre, style: :display },
+ { pattern: DOLLAR_INLINE_PATTERN, tag: :code, style: :inline }
+ ].freeze
+
+ # Do not recognize math inside these tags
+ IGNORED_ANCESTOR_TAGS = %w[pre code tt].to_set
+
# Attribute indicating inline or display math.
STYLE_ATTRIBUTE = 'data-math-style'
# Class used for tagging elements that should be rendered
TAG_CLASS = 'js-render-math'
- INLINE_CLASSES = "code math #{TAG_CLASS}"
-
+ MATH_CLASSES = "code math #{TAG_CLASS}"
DOLLAR_SIGN = '$'
# Limit to how many nodes can be marked as math elements.
@@ -31,8 +59,48 @@ module Banzai
RENDER_NODES_LIMIT = 50
def call
- nodes_count = 0
+ @nodes_count = 0
+
+ process_dollar_pipeline if Feature.enabled?(:markdown_dollar_math, group)
+
+ process_dollar_backtick_inline
+ process_math_codeblock
+
+ doc
+ end
+
+ def process_dollar_pipeline
+ doc.xpath('descendant-or-self::text()').each do |node|
+ next if has_ancestor?(node, IGNORED_ANCESTOR_TAGS)
+
+ node_html = node.to_html
+ next unless node_html.match?(DOLLAR_INLINE_PATTERN) ||
+ node_html.match?(DOLLAR_DISPLAY_INLINE_PATTERN) ||
+ node_html.match?(DOLLAR_DISPLAY_BLOCK_PATTERN)
+
+ temp_doc = Nokogiri::HTML.fragment(node_html)
+ DOLLAR_MATH_PIPELINE.each do |pipeline|
+ temp_doc.xpath('child::text()').each do |temp_node|
+ html = temp_node.to_html
+ temp_node.content.scan(pipeline[:pattern]).each do |matched, math|
+ html.sub!(matched, math_html(tag: pipeline[:tag], style: pipeline[:style], math: math))
+ @nodes_count += 1
+ break if @nodes_count >= RENDER_NODES_LIMIT
+ end
+
+ temp_node.replace(html)
+
+ break if @nodes_count >= RENDER_NODES_LIMIT
+ end
+ end
+
+ node.replace(temp_doc)
+ end
+ end
+
+ # Corresponds to the "$`...`$" syntax
+ def process_dollar_backtick_inline
doc.xpath(XPATH_CODE).each do |code|
closing = code.next
opening = code.previous
@@ -44,22 +112,38 @@ module Banzai
closing.content.first == DOLLAR_SIGN &&
opening.content.last == DOLLAR_SIGN
- code[:class] = INLINE_CLASSES
+ code[:class] = MATH_CLASSES
code[STYLE_ATTRIBUTE] = 'inline'
closing.content = closing.content[1..]
opening.content = opening.content[0..-2]
- nodes_count += 1
- break if nodes_count >= RENDER_NODES_LIMIT
+ @nodes_count += 1
+ break if @nodes_count >= RENDER_NODES_LIMIT
end
end
+ end
+ # corresponds to the "```math...```" syntax
+ def process_math_codeblock
doc.xpath(XPATH_MATH).each do |el|
el[STYLE_ATTRIBUTE] = 'display'
el[:class] += " #{TAG_CLASS}"
end
+ end
- doc
+ private
+
+ def math_html(tag:, math:, style:)
+ case tag
+ when :code
+ "<code class=\"#{MATH_CLASSES}\" data-math-style=\"#{style}\">#{math}</code>"
+ when :pre
+ "<pre class=\"#{MATH_CLASSES}\" data-math-style=\"#{style}\"><code>#{math}</code></pre>"
+ end
+ end
+
+ def group
+ context[:group] || context[:project]&.group
end
end
end
diff --git a/lib/banzai/filter/plantuml_filter.rb b/lib/banzai/filter/plantuml_filter.rb
index cbcd547120d..82f6247cf03 100644
--- a/lib/banzai/filter/plantuml_filter.rb
+++ b/lib/banzai/filter/plantuml_filter.rb
@@ -31,7 +31,8 @@ module Banzai
private
def lang_tag
- @lang_tag ||= Gitlab::Utils::Nokogiri.css_to_xpath('pre[lang="plantuml"] > code').freeze
+ @lang_tag ||= Gitlab::Utils::Nokogiri
+ .css_to_xpath('pre[lang="plantuml"] > code, pre > code[lang="plantuml"]').freeze
end
def settings
diff --git a/lib/banzai/filter/references/reference_filter.rb b/lib/banzai/filter/references/reference_filter.rb
index 97ef71036a2..37734f6a45a 100644
--- a/lib/banzai/filter/references/reference_filter.rb
+++ b/lib/banzai/filter/references/reference_filter.rb
@@ -15,6 +15,8 @@ module Banzai
include RequestStoreReferenceCache
include OutputSafety
+ REFERENCE_TYPE_DATA_ATTRIBUTE = 'data-reference-type='
+
class << self
# Implement in child class
# Example: self.reference_type = :merge_request
@@ -132,13 +134,19 @@ module Banzai
def data_attribute(attributes = {})
attributes = attributes.reject { |_, v| v.nil? }
- attributes[:reference_type] ||= self.class.reference_type
+ # "data-reference-type=" attribute got moved into a constant because we need
+ # to use it on ReferenceRewriter class to detect if the markdown contains any reference
+ reference_type_attribute = "#{REFERENCE_TYPE_DATA_ATTRIBUTE}#{escape_once(self.class.reference_type)} "
+
attributes[:container] ||= 'body'
attributes[:placement] ||= 'top'
attributes.delete(:original) if context[:no_original_data]
+
attributes.map do |key, value|
%Q(data-#{key.to_s.dasherize}="#{escape_once(value)}")
- end.join(' ')
+ end
+ .join(' ')
+ .prepend(reference_type_attribute)
end
def ignore_ancestor_query
diff --git a/lib/banzai/pipeline/markup_pipeline.rb b/lib/banzai/pipeline/markup_pipeline.rb
index 17a73f29afb..330914f7238 100644
--- a/lib/banzai/pipeline/markup_pipeline.rb
+++ b/lib/banzai/pipeline/markup_pipeline.rb
@@ -9,8 +9,8 @@ module Banzai
Filter::AssetProxyFilter,
Filter::ExternalLinkFilter,
Filter::PlantumlFilter,
- Filter::SyntaxHighlightFilter,
- Filter::KrokiFilter
+ Filter::KrokiFilter,
+ Filter::SyntaxHighlightFilter
]
end
diff --git a/lib/bulk_imports/file_downloads/filename_fetch.rb b/lib/bulk_imports/file_downloads/filename_fetch.rb
new file mode 100644
index 00000000000..b6bb0fd8c81
--- /dev/null
+++ b/lib/bulk_imports/file_downloads/filename_fetch.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+module BulkImports
+ module FileDownloads
+ module FilenameFetch
+ REMOTE_FILENAME_PATTERN = %r{filename="(?<filename>[^"]+)"}.freeze
+ FILENAME_SIZE_LIMIT = 255 # chars before the extension
+
+ def raise_error(message)
+ raise NotImplementedError
+ end
+
+ private
+
+ # Fetch the remote filename information from the request content-disposition header
+ # - Raises if the filename does not exist
+ # - If the filename is longer then 255 chars truncate it
+ # to be a total of 255 chars (with the extension)
+ # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ def remote_filename
+ @remote_filename ||= begin
+ pattern = BulkImports::FileDownloads::FilenameFetch::REMOTE_FILENAME_PATTERN
+ name = response_headers['content-disposition'].to_s
+ .match(pattern) # matches the filename pattern
+ .then { |match| match&.named_captures || {} } # ensures the match is a hash
+ .fetch('filename') # fetches the 'filename' key or raise KeyError
+
+ name = File.basename(name) # Ensures to remove path from the filename (../ for instance)
+ ensure_filename_size(name) # Ensures the filename is within the FILENAME_SIZE_LIMIT
+ end
+ rescue KeyError
+ raise_error 'Remote filename not provided in content-disposition header'
+ end
+ # rubocop:enable Gitlab/ModuleWithInstanceVariables
+
+ def ensure_filename_size(filename)
+ limit = BulkImports::FileDownloads::FilenameFetch::FILENAME_SIZE_LIMIT
+ return filename if filename.length <= limit
+
+ extname = File.extname(filename)
+ basename = File.basename(filename, extname)[0, limit]
+ "#{basename}#{extname}"
+ end
+ end
+ end
+end
diff --git a/lib/bulk_imports/file_downloads/validations.rb b/lib/bulk_imports/file_downloads/validations.rb
new file mode 100644
index 00000000000..ae94267a6e8
--- /dev/null
+++ b/lib/bulk_imports/file_downloads/validations.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+module BulkImports
+ module FileDownloads
+ module Validations
+ def raise_error(message)
+ raise NotImplementedError
+ end
+
+ def filepath
+ raise NotImplementedError
+ end
+
+ def file_size_limit
+ raise NotImplementedError
+ end
+
+ def response_headers
+ raise NotImplementedError
+ end
+
+ private
+
+ def validate_filepath
+ Gitlab::Utils.check_path_traversal!(filepath)
+ end
+
+ def validate_content_type
+ content_type = response_headers['content-type']
+
+ raise_error('Invalid content type') if content_type.blank? || allowed_content_types.exclude?(content_type)
+ end
+
+ def validate_symlink
+ return unless File.lstat(filepath).symlink?
+
+ File.delete(filepath)
+ raise_error 'Invalid downloaded file'
+ end
+
+ def validate_content_length
+ validate_size!(response_headers['content-length'])
+ end
+
+ def validate_size!(size)
+ if size.blank?
+ raise_error 'Missing content-length header'
+ elsif size.to_i > file_size_limit
+ raise_error format(
+ "File size %{size} exceeds limit of %{limit}",
+ size: ActiveSupport::NumberHelper.number_to_human_size(size),
+ limit: ActiveSupport::NumberHelper.number_to_human_size(file_size_limit)
+ )
+ end
+ end
+ end
+ end
+end
diff --git a/lib/container_registry/gitlab_api_client.rb b/lib/container_registry/gitlab_api_client.rb
index be99fa75ffe..2947dcb4b40 100644
--- a/lib/container_registry/gitlab_api_client.rb
+++ b/lib/container_registry/gitlab_api_client.rb
@@ -31,7 +31,7 @@ module ContainerRegistry
def self.deduplicated_size(path)
with_dummy_client(token_config: { type: :nested_repositories_token, path: path&.downcase }) do |client|
- client.repository_details(path, sizing: :self_with_descendants)['size_bytes']
+ client.repository_details(path&.downcase, sizing: :self_with_descendants)['size_bytes']
end
end
diff --git a/lib/container_registry/tag.rb b/lib/container_registry/tag.rb
index 76188a937c0..bf44b74cf7b 100644
--- a/lib/container_registry/tag.rb
+++ b/lib/container_registry/tag.rb
@@ -4,7 +4,7 @@ module ContainerRegistry
class Tag
include Gitlab::Utils::StrongMemoize
- attr_reader :repository, :name
+ attr_reader :repository, :name, :updated_at
attr_writer :created_at
delegate :registry, :client, to: :repository
@@ -97,6 +97,17 @@ module ContainerRegistry
instance_variable_set(ivar(:memoized_created_at), date)
end
+ def updated_at=(string_value)
+ return unless string_value
+
+ @updated_at =
+ begin
+ DateTime.iso8601(string_value)
+ rescue ArgumentError
+ nil
+ end
+ end
+
def layers
return unless manifest
diff --git a/lib/error_tracking/sentry_client.rb b/lib/error_tracking/sentry_client.rb
index 6a341ddbe86..029389ab5d6 100644
--- a/lib/error_tracking/sentry_client.rb
+++ b/lib/error_tracking/sentry_client.rb
@@ -10,16 +10,32 @@ module ErrorTracking
Error = Class.new(StandardError)
MissingKeysError = Class.new(StandardError)
+ ResponseInvalidSizeError = Class.new(StandardError)
+
+ RESPONSE_SIZE_LIMIT = 1.megabyte
attr_accessor :url, :token
- def initialize(api_url, token)
+ def initialize(api_url, token, validate_size_guarded_by_feature_flag: false)
@url = api_url
@token = token
+ @validate_size_guarded_by_feature_flag = validate_size_guarded_by_feature_flag
+ end
+
+ def validate_size_guarded_by_feature_flag?
+ @validate_size_guarded_by_feature_flag
end
private
+ def validate_size(response)
+ return if Gitlab::Utils::DeepSize.new(response, max_size: RESPONSE_SIZE_LIMIT).valid?
+
+ limit = ActiveSupport::NumberHelper.number_to_human_size(RESPONSE_SIZE_LIMIT)
+ message = "Sentry API response is too big. Limit is #{limit}."
+ raise ResponseInvalidSizeError, message
+ end
+
def api_urls
@api_urls ||= SentryClient::ApiUrls.new(@url)
end
@@ -86,6 +102,8 @@ module ErrorTracking
def handle_response(response)
raise_error "Sentry response status code: #{response.code}" unless response.code.between?(200, 204)
+ validate_size(response.parsed_response) if validate_size_guarded_by_feature_flag?
+
{ body: response.parsed_response, headers: response.headers }
end
diff --git a/lib/error_tracking/sentry_client/issue.rb b/lib/error_tracking/sentry_client/issue.rb
index d0e6bd783f3..3c846eb0635 100644
--- a/lib/error_tracking/sentry_client/issue.rb
+++ b/lib/error_tracking/sentry_client/issue.rb
@@ -4,7 +4,6 @@ module ErrorTracking
class SentryClient
module Issue
BadRequestError = Class.new(StandardError)
- ResponseInvalidSizeError = Class.new(StandardError)
SENTRY_API_SORT_VALUE_MAP = {
# <accepted_by_client> => <accepted_by_sentry_api>
@@ -19,7 +18,9 @@ module ErrorTracking
issues = response[:issues]
pagination = response[:pagination]
- validate_size(issues)
+ # We check validate size only with feture flag disabled because when
+ # enabled we already check it when parsing the response.
+ validate_size(issues) unless validate_size_guarded_by_feature_flag?
handle_mapping_exceptions do
{
@@ -64,13 +65,6 @@ module ErrorTracking
}.compact
end
- def validate_size(issues)
- return if Gitlab::Utils::DeepSize.new(issues).valid?
-
- message = "Sentry API response is too big. Limit is #{Gitlab::Utils::DeepSize.human_default_max_size}."
- raise ResponseInvalidSizeError, message
- end
-
def get_issue(issue_id:)
http_get(api_urls.issue_url(issue_id))[:body]
end
diff --git a/lib/event_filter.rb b/lib/event_filter.rb
index 8c3377fdb80..f14b0a6b9e7 100644
--- a/lib/event_filter.rb
+++ b/lib/event_filter.rb
@@ -131,18 +131,19 @@ class EventFilter
finder_query = -> (id_expression) { Event.where(Event.arel_table[:id].eq(id_expression)) }
if order_hint_column.present?
- order = Gitlab::Pagination::Keyset::Order.build([
- Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
- attribute_name: order_hint_column,
- order_expression: Event.arel_table[order_hint_column].desc,
- nullable: :nulls_last,
- distinct: false
- ),
- Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
- attribute_name: :id,
- order_expression: Event.arel_table[:id].desc
- )
- ])
+ order = Gitlab::Pagination::Keyset::Order.build(
+ [
+ Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
+ attribute_name: order_hint_column,
+ order_expression: Event.arel_table[order_hint_column].desc,
+ nullable: :nulls_last,
+ distinct: false
+ ),
+ Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
+ attribute_name: :id,
+ order_expression: Event.arel_table[:id].desc
+ )
+ ])
finder_query = -> (_order_hint, id_expression) { Event.where(Event.arel_table[:id].eq(id_expression)) }
end
diff --git a/lib/generators/gitlab/usage_metric/templates/instrumentation_class_spec.rb.template b/lib/generators/gitlab/usage_metric/templates/instrumentation_class_spec.rb.template
index e984daee0a4..f8bd502ab77 100644
--- a/lib/generators/gitlab/usage_metric/templates/instrumentation_class_spec.rb.template
+++ b/lib/generators/gitlab/usage_metric/templates/instrumentation_class_spec.rb.template
@@ -3,5 +3,7 @@
require 'spec_helper'
RSpec.describe Gitlab::Usage::Metrics::Instrumentations::<%= class_name %>Metric do
- it_behaves_like 'a correct instrumented metric value', {}, 1
+ let(:expected_value) { 1 }
+
+ it_behaves_like 'a correct instrumented metric value', { time_frame: 'all', data_source: 'database' }
end
diff --git a/lib/gitlab/abuse.rb b/lib/gitlab/abuse.rb
index cc95d3c1e0c..7db99d4b037 100644
--- a/lib/gitlab/abuse.rb
+++ b/lib/gitlab/abuse.rb
@@ -3,10 +3,10 @@
module Gitlab
module Abuse
CONFIDENCE_LEVELS = {
- certain: 1.0,
- likely: 0.8,
+ certain: 1.0,
+ likely: 0.8,
uncertain: 0.5,
- unknown: 0.0
+ unknown: 0.0
}.freeze
class << self
diff --git a/lib/gitlab/access.rb b/lib/gitlab/access.rb
index 3e09d488bc3..fa025a2658f 100644
--- a/lib/gitlab/access.rb
+++ b/lib/gitlab/access.rb
@@ -41,9 +41,9 @@ module Gitlab
def options
{
- "Guest" => GUEST,
- "Reporter" => REPORTER,
- "Developer" => DEVELOPER,
+ "Guest" => GUEST,
+ "Reporter" => REPORTER,
+ "Developer" => DEVELOPER,
"Maintainer" => MAINTAINER
}
end
@@ -62,9 +62,9 @@ module Gitlab
def sym_options
{
- guest: GUEST,
- reporter: REPORTER,
- developer: DEVELOPER,
+ guest: GUEST,
+ reporter: REPORTER,
+ developer: DEVELOPER,
maintainer: MAINTAINER
}
end
@@ -120,9 +120,9 @@ module Gitlab
def project_creation_string_options
{
- 'noone' => NO_ONE_PROJECT_ACCESS,
- 'maintainer' => MAINTAINER_PROJECT_ACCESS,
- 'developer' => DEVELOPER_MAINTAINER_PROJECT_ACCESS
+ 'noone' => NO_ONE_PROJECT_ACCESS,
+ 'maintainer' => MAINTAINER_PROJECT_ACCESS,
+ 'developer' => DEVELOPER_MAINTAINER_PROJECT_ACCESS
}
end
@@ -147,7 +147,7 @@ module Gitlab
def subgroup_creation_string_options
{
- 'owner' => OWNER_SUBGROUP_ACCESS,
+ 'owner' => OWNER_SUBGROUP_ACCESS,
'maintainer' => MAINTAINER_SUBGROUP_ACCESS
}
end
diff --git a/lib/gitlab/alert_management/payload/base.rb b/lib/gitlab/alert_management/payload/base.rb
index 2d769148c5f..01dcb95eab5 100644
--- a/lib/gitlab/alert_management/payload/base.rb
+++ b/lib/gitlab/alert_management/payload/base.rb
@@ -149,6 +149,10 @@ module Gitlab
severity_mapping.fetch(severity_raw.to_s.downcase, UNMAPPED_SEVERITY)
end
+ def source
+ monitoring_tool || integration&.name
+ end
+
private
def plain_gitlab_fingerprint
diff --git a/lib/gitlab/alert_management/payload/generic.rb b/lib/gitlab/alert_management/payload/generic.rb
index 15238b5e50f..18e65779ead 100644
--- a/lib/gitlab/alert_management/payload/generic.rb
+++ b/lib/gitlab/alert_management/payload/generic.rb
@@ -6,6 +6,7 @@ module Gitlab
module Payload
class Generic < Base
DEFAULT_TITLE = 'New: Alert'
+ DEFAULT_SOURCE = 'Generic Alert Endpoint'
attribute :description, paths: 'description'
attribute :ends_at, paths: 'end_time', type: :time
@@ -22,6 +23,14 @@ module Gitlab
attribute :plain_gitlab_fingerprint, paths: 'fingerprint'
private :plain_gitlab_fingerprint
+
+ def resolved?
+ ends_at.present?
+ end
+
+ def source
+ super || DEFAULT_SOURCE
+ end
end
end
end
diff --git a/lib/gitlab/analytics/cycle_analytics/request_params.rb b/lib/gitlab/analytics/cycle_analytics/request_params.rb
index d0d8d68362e..ac9c465bf7d 100644
--- a/lib/gitlab/analytics/cycle_analytics/request_params.rb
+++ b/lib/gitlab/analytics/cycle_analytics/request_params.rb
@@ -105,9 +105,8 @@ module Gitlab
private
def use_aggregated_backend?
- group.present? && # for now it's only available on the group-level
- aggregation.enabled &&
- Feature.enabled?(:use_vsa_aggregated_tables, group)
+ # for now it's only available on the group-level
+ group.present? && aggregation.enabled
end
def aggregation_attributes
diff --git a/lib/gitlab/analytics/date_filler.rb b/lib/gitlab/analytics/date_filler.rb
new file mode 100644
index 00000000000..aa3db9f3635
--- /dev/null
+++ b/lib/gitlab/analytics/date_filler.rb
@@ -0,0 +1,112 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Analytics
+ # This class generates a date => value hash without gaps in the data points.
+ #
+ # Simple usage:
+ #
+ # > # We have the following data for the last 5 day:
+ # > input = { 3.days.ago.to_date => 10, Date.today => 5 }
+ #
+ # > # Format this data, so we can chart the complete date range:
+ # > Gitlab::Analytics::DateFiller.new(input, from: 4.days.ago, to: Date.today, default_value: 0).fill
+ # > {
+ # > Sun, 28 Aug 2022=>0,
+ # > Mon, 29 Aug 2022=>10,
+ # > Tue, 30 Aug 2022=>0,
+ # > Wed, 31 Aug 2022=>0,
+ # > Thu, 01 Sep 2022=>5
+ # > }
+ #
+ # Parameters:
+ #
+ # **input**
+ # A Hash containing data for the series or the chart. The key is a Date object
+ # or an object which can be converted to Date.
+ #
+ # **from**
+ # Start date of the range
+ #
+ # **to**
+ # End date of the range
+ #
+ # **period**
+ # Specifies the period in wich the dates should be generated. Options:
+ #
+ # - :day, generate date-value pair for each day in the given period
+ # - :week, generate date-value pair for each week (beginning of the week date)
+ # - :month, generate date-value pair for each week (beginning of the month date)
+ #
+ # Note: the Date objects in the `input` should follow the same pattern (beginning of ...)
+ #
+ # **default_value**
+ #
+ # Which value use when the `input` Hash does not contain data for the given day.
+ #
+ # **date_formatter**
+ #
+ # How to format the dates in the resulting hash.
+ class DateFiller
+ DEFAULT_DATE_FORMATTER = -> (date) { date }
+ PERIOD_STEPS = {
+ day: 1.day,
+ week: 1.week,
+ month: 1.month
+ }.freeze
+
+ def initialize(
+ input,
+ from:,
+ to:,
+ period: :day,
+ default_value: nil,
+ date_formatter: DEFAULT_DATE_FORMATTER)
+ @input = input.transform_keys(&:to_date)
+ @from = from.to_date
+ @to = to.to_date
+ @period = period
+ @default_value = default_value
+ @date_formatter = date_formatter
+ end
+
+ def fill
+ data = {}
+
+ current_date = from
+ loop do
+ transformed_date = transform_date(current_date)
+ break if transformed_date > to
+
+ formatted_date = date_formatter.call(transformed_date)
+
+ value = input.delete(transformed_date)
+ data[formatted_date] = value.nil? ? default_value : value
+
+ current_date = (current_date + PERIOD_STEPS.fetch(period)).to_date
+ end
+
+ raise "Input contains values which doesn't fall under the given period!" if input.any?
+
+ data
+ end
+
+ private
+
+ attr_reader :input, :from, :to, :period, :default_value, :date_formatter
+
+ def transform_date(date)
+ case period
+ when :day
+ date.beginning_of_day.to_date
+ when :week
+ date.beginning_of_week.to_date
+ when :month
+ date.beginning_of_month.to_date
+ else
+ raise "Unknown period given: #{period}"
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/application_rate_limiter.rb b/lib/gitlab/application_rate_limiter.rb
index a2d79b189a3..507f94d87a5 100644
--- a/lib/gitlab/application_rate_limiter.rb
+++ b/lib/gitlab/application_rate_limiter.rb
@@ -16,40 +16,43 @@ module Gitlab
# and only do that when it's needed.
def rate_limits # rubocop:disable Metrics/AbcSize
{
- issues_create: { threshold: -> { application_settings.issues_create_limit }, interval: 1.minute },
- notes_create: { threshold: -> { application_settings.notes_create_limit }, interval: 1.minute },
- project_export: { threshold: -> { application_settings.project_export_limit }, interval: 1.minute },
- project_download_export: { threshold: -> { application_settings.project_download_export_limit }, interval: 1.minute },
+ issues_create: { threshold: -> { application_settings.issues_create_limit }, interval: 1.minute },
+ notes_create: { threshold: -> { application_settings.notes_create_limit }, interval: 1.minute },
+ project_export: { threshold: -> { application_settings.project_export_limit }, interval: 1.minute },
+ project_download_export: { threshold: -> { application_settings.project_download_export_limit }, interval: 1.minute },
project_repositories_archive: { threshold: 5, interval: 1.minute },
- project_generate_new_export: { threshold: -> { application_settings.project_export_limit }, interval: 1.minute },
- project_import: { threshold: -> { application_settings.project_import_limit }, interval: 1.minute },
- project_testing_hook: { threshold: 5, interval: 1.minute },
- play_pipeline_schedule: { threshold: 1, interval: 1.minute },
- raw_blob: { threshold: -> { application_settings.raw_blob_request_limit }, interval: 1.minute },
- group_export: { threshold: -> { application_settings.group_export_limit }, interval: 1.minute },
- group_download_export: { threshold: -> { application_settings.group_download_export_limit }, interval: 1.minute },
- group_import: { threshold: -> { application_settings.group_import_limit }, interval: 1.minute },
- group_testing_hook: { threshold: 5, interval: 1.minute },
- profile_add_new_email: { threshold: 5, interval: 1.minute },
- web_hook_calls: { interval: 1.minute },
- web_hook_calls_mid: { interval: 1.minute },
- web_hook_calls_low: { interval: 1.minute },
- users_get_by_id: { threshold: -> { application_settings.users_get_by_id_limit }, interval: 10.minutes },
- username_exists: { threshold: 20, interval: 1.minute },
- user_sign_up: { threshold: 20, interval: 1.minute },
- user_sign_in: { threshold: 5, interval: 10.minutes },
- profile_resend_email_confirmation: { threshold: 5, interval: 1.minute },
- profile_update_username: { threshold: 10, interval: 1.minute },
- update_environment_canary_ingress: { threshold: 1, interval: 1.minute },
- auto_rollback_deployment: { threshold: 1, interval: 3.minutes },
- search_rate_limit: { threshold: -> { application_settings.search_rate_limit }, interval: 1.minute },
- search_rate_limit_unauthenticated: { threshold: -> { application_settings.search_rate_limit_unauthenticated }, interval: 1.minute },
- gitlab_shell_operation: { threshold: 600, interval: 1.minute },
- pipelines_create: { threshold: -> { application_settings.pipeline_limit_per_project_user_sha }, interval: 1.minute },
- temporary_email_failure: { threshold: 50, interval: 1.day },
- project_testing_integration: { threshold: 5, interval: 1.minute },
- email_verification: { threshold: 10, interval: 10.minutes },
- email_verification_code_send: { threshold: 10, interval: 1.hour }
+ project_generate_new_export: { threshold: -> { application_settings.project_export_limit }, interval: 1.minute },
+ project_import: { threshold: -> { application_settings.project_import_limit }, interval: 1.minute },
+ project_testing_hook: { threshold: 5, interval: 1.minute },
+ play_pipeline_schedule: { threshold: 1, interval: 1.minute },
+ raw_blob: { threshold: -> { application_settings.raw_blob_request_limit }, interval: 1.minute },
+ group_export: { threshold: -> { application_settings.group_export_limit }, interval: 1.minute },
+ group_download_export: { threshold: -> { application_settings.group_download_export_limit }, interval: 1.minute },
+ group_import: { threshold: -> { application_settings.group_import_limit }, interval: 1.minute },
+ group_testing_hook: { threshold: 5, interval: 1.minute },
+ profile_add_new_email: { threshold: 5, interval: 1.minute },
+ web_hook_calls: { interval: 1.minute },
+ web_hook_calls_mid: { interval: 1.minute },
+ web_hook_calls_low: { interval: 1.minute },
+ users_get_by_id: { threshold: -> { application_settings.users_get_by_id_limit }, interval: 10.minutes },
+ username_exists: { threshold: 20, interval: 1.minute },
+ user_sign_up: { threshold: 20, interval: 1.minute },
+ user_sign_in: { threshold: 5, interval: 10.minutes },
+ profile_resend_email_confirmation: { threshold: 5, interval: 1.minute },
+ profile_update_username: { threshold: 10, interval: 1.minute },
+ update_environment_canary_ingress: { threshold: 1, interval: 1.minute },
+ auto_rollback_deployment: { threshold: 1, interval: 3.minutes },
+ search_rate_limit: { threshold: -> { application_settings.search_rate_limit }, interval: 1.minute },
+ search_rate_limit_unauthenticated: { threshold: -> { application_settings.search_rate_limit_unauthenticated }, interval: 1.minute },
+ gitlab_shell_operation: { threshold: 600, interval: 1.minute },
+ pipelines_create: { threshold: -> { application_settings.pipeline_limit_per_project_user_sha }, interval: 1.minute },
+ temporary_email_failure: { threshold: 300, interval: 1.day },
+ permanent_email_failure: { threshold: 5, interval: 1.day },
+ project_testing_integration: { threshold: 5, interval: 1.minute },
+ email_verification: { threshold: 10, interval: 10.minutes },
+ email_verification_code_send: { threshold: 10, interval: 1.hour },
+ namespace_exists: { threshold: 20, interval: 1.minute },
+ fetch_google_ip_list: { threshold: 10, interval: 1.minute }
}.freeze
end
@@ -130,16 +133,16 @@ module Gitlab
# @param logger [Logger] Logger to log request to a specific log file. Defaults to Gitlab::AuthLogger
def log_request(request, type, current_user, logger = Gitlab::AuthLogger)
request_information = {
- message: 'Application_Rate_Limiter_Request',
- env: type,
- remote_ip: request.ip,
+ message: 'Application_Rate_Limiter_Request',
+ env: type,
+ remote_ip: request.ip,
request_method: request.request_method,
- path: request.fullpath
+ path: request.fullpath
}
if current_user
request_information.merge!({
- user_id: current_user.id,
+ user_id: current_user.id,
username: current_user.username
})
end
diff --git a/lib/gitlab/application_rate_limiter/increment_per_action.rb b/lib/gitlab/application_rate_limiter/increment_per_action.rb
index c99d03f1344..a3343c8a97c 100644
--- a/lib/gitlab/application_rate_limiter/increment_per_action.rb
+++ b/lib/gitlab/application_rate_limiter/increment_per_action.rb
@@ -5,9 +5,9 @@ module Gitlab
class IncrementPerAction < BaseStrategy
def increment(cache_key, expiry)
with_redis do |redis|
- redis.pipelined do
- redis.incr(cache_key)
- redis.expire(cache_key, expiry)
+ redis.pipelined do |pipeline|
+ pipeline.incr(cache_key)
+ pipeline.expire(cache_key, expiry)
end.first
end
end
diff --git a/lib/gitlab/application_rate_limiter/increment_per_actioned_resource.rb b/lib/gitlab/application_rate_limiter/increment_per_actioned_resource.rb
index 8b4197cfff9..7a68dd104a8 100644
--- a/lib/gitlab/application_rate_limiter/increment_per_actioned_resource.rb
+++ b/lib/gitlab/application_rate_limiter/increment_per_actioned_resource.rb
@@ -9,10 +9,10 @@ module Gitlab
def increment(cache_key, expiry)
with_redis do |redis|
- redis.pipelined do
- redis.sadd(cache_key, resource_key)
- redis.expire(cache_key, expiry)
- redis.scard(cache_key)
+ redis.pipelined do |pipeline|
+ pipeline.sadd(cache_key, resource_key)
+ pipeline.expire(cache_key, expiry)
+ pipeline.scard(cache_key)
end.last
end
end
diff --git a/lib/gitlab/audit/auditor.rb b/lib/gitlab/audit/auditor.rb
index c96be19f02d..4a6e4e2e06e 100644
--- a/lib/gitlab/audit/auditor.rb
+++ b/lib/gitlab/audit/auditor.rb
@@ -117,7 +117,7 @@ module Gitlab
# Only capture real users for successful authentication events.
user: author_if_user,
user_name: @author.name,
- ip_address: @ip_address,
+ ip_address: Gitlab::RequestContext.instance.client_ip || @author.current_sign_in_ip,
result: AuthenticationEvent.results[:success],
provider: @authentication_provider
}
diff --git a/lib/gitlab/audit/type/definition.rb b/lib/gitlab/audit/type/definition.rb
new file mode 100644
index 00000000000..af5dc9f4b44
--- /dev/null
+++ b/lib/gitlab/audit/type/definition.rb
@@ -0,0 +1,122 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Audit
+ module Type
+ class Definition
+ include ActiveModel::Validations
+
+ attr_reader :path
+ attr_reader :attributes
+
+ validate :validate_schema
+ validate :validate_file_name
+
+ InvalidAuditEventTypeError = Class.new(StandardError)
+
+ AUDIT_EVENT_TYPE_SCHEMA_PATH = Rails.root.join('config', 'audit_events', 'types', 'type_schema.json')
+ AUDIT_EVENT_TYPE_SCHEMA = JSONSchemer.schema(AUDIT_EVENT_TYPE_SCHEMA_PATH)
+
+ # The PARAMS in config/audit_events/types/type_schema.json
+ PARAMS = %i[
+ name
+ description
+ introduced_by_issue
+ introduced_by_mr
+ group
+ milestone
+ saved_to_database
+ streamed
+ ].freeze
+
+ PARAMS.each do |param|
+ define_method(param) do
+ attributes[param]
+ end
+ end
+
+ def initialize(path, opts = {})
+ @path = path
+ @attributes = {}
+
+ # assign nil, for all unknown opts
+ PARAMS.each do |param|
+ @attributes[param] = opts[param]
+ end
+ end
+
+ def key
+ name.to_sym
+ end
+
+ private
+
+ def validate_schema
+ schema_errors = AUDIT_EVENT_TYPE_SCHEMA
+ .validate(attributes.to_h.deep_stringify_keys)
+ .map { |error| JSONSchemer::Errors.pretty(error) }
+
+ errors.add(:base, schema_errors) if schema_errors.present?
+ end
+
+ def validate_file_name
+ # ignoring Style/GuardClause because if we move this into one line, we cause Layout/LineLength errors
+ # rubocop:disable Style/GuardClause
+ unless File.basename(path, ".yml") == name
+ errors.add(:base, "Audit event type '#{name}' has an invalid path: '#{path}'. " \
+ "'#{name}' must match the filename")
+ end
+ # rubocop:enable Style/GuardClause
+ end
+
+ class << self
+ def paths
+ @paths ||= [Rails.root.join('config', 'audit_events', 'types', '*.yml')]
+ end
+
+ def definitions
+ # We lazily load all definitions
+ @definitions ||= load_all!
+ end
+
+ def get(key)
+ definitions[key.to_sym]
+ end
+
+ private
+
+ def load_all!
+ paths.each_with_object({}) do |glob_path, definitions|
+ load_all_from_path!(definitions, glob_path)
+ end
+ end
+
+ def load_all_from_path!(definitions, glob_path)
+ Dir.glob(glob_path).each do |path|
+ definition = load_from_file(path)
+
+ if previous = definitions[definition.key]
+ raise InvalidAuditEventTypeError, "Audit event type '#{definition.key}' " \
+ "is already defined in '#{previous.path}'"
+ end
+
+ definitions[definition.key] = definition
+ end
+ end
+
+ def load_from_file(path)
+ definition = File.read(path)
+ definition = YAML.safe_load(definition)
+ definition.deep_symbolize_keys!
+
+ new(path, definition).tap(&:validate!)
+ rescue StandardError => e
+ raise InvalidAuditEventTypeError, "Invalid definition for `#{path}`: #{e.message}"
+ end
+ end
+ end
+ end
+ end
+end
+
+Gitlab::Audit::Type::Definition.prepend_mod
diff --git a/lib/gitlab/auth/ldap/config.rb b/lib/gitlab/auth/ldap/config.rb
index 82c6411c712..9dafd59561a 100644
--- a/lib/gitlab/auth/ldap/config.rb
+++ b/lib/gitlab/auth/ldap/config.rb
@@ -7,8 +7,8 @@ module Gitlab
class Config
NET_LDAP_ENCRYPTION_METHOD = {
simple_tls: :simple_tls,
- start_tls: :start_tls,
- plain: nil
+ start_tls: :start_tls,
+ plain: nil
}.freeze
attr_accessor :provider, :options
@@ -193,11 +193,11 @@ module Gitlab
def default_attributes
{
- 'username' => %W(#{uid} uid sAMAccountName userid).uniq,
- 'email' => %w(mail email userPrincipalName),
- 'name' => 'cn',
- 'first_name' => 'givenName',
- 'last_name' => 'sn'
+ 'username' => %W(#{uid} uid sAMAccountName userid).uniq,
+ 'email' => %w(mail email userPrincipalName),
+ 'name' => 'cn',
+ 'first_name' => 'givenName',
+ 'last_name' => 'sn'
}
end
diff --git a/lib/gitlab/auth/o_auth/auth_hash.rb b/lib/gitlab/auth/o_auth/auth_hash.rb
index 37f92792d2d..82a5aad360c 100644
--- a/lib/gitlab/auth/o_auth/auth_hash.rb
+++ b/lib/gitlab/auth/o_auth/auth_hash.rb
@@ -33,7 +33,7 @@ module Gitlab
end
def password
- @password ||= Gitlab::Utils.force_utf8(::User.random_password.downcase)
+ @password ||= Gitlab::Utils.force_utf8(::User.random_password)
end
def location
@@ -103,7 +103,7 @@ module Gitlab
{
username: username,
- email: email
+ email: email
}
end
end
diff --git a/lib/gitlab/auth/o_auth/provider.rb b/lib/gitlab/auth/o_auth/provider.rb
index 1a25ed10d81..2ce8677c8b7 100644
--- a/lib/gitlab/auth/o_auth/provider.rb
+++ b/lib/gitlab/auth/o_auth/provider.rb
@@ -5,14 +5,14 @@ module Gitlab
module OAuth
class Provider
LABELS = {
- "alicloud" => "AliCloud",
- "dingtalk" => "DingTalk",
- "github" => "GitHub",
- "gitlab" => "GitLab.com",
- "google_oauth2" => "Google",
- "azure_oauth2" => "Azure AD",
+ "alicloud" => "AliCloud",
+ "dingtalk" => "DingTalk",
+ "github" => "GitHub",
+ "gitlab" => "GitLab.com",
+ "google_oauth2" => "Google",
+ "azure_oauth2" => "Azure AD",
"azure_activedirectory_v2" => "Azure AD v2",
- 'atlassian_oauth2' => 'Atlassian'
+ 'atlassian_oauth2' => 'Atlassian'
}.freeze
def self.authentication(user, provider)
@@ -68,7 +68,9 @@ module Gitlab
nil
end
else
- provider = Gitlab.config.omniauth.providers.find { |provider| provider.name == name }
+ provider = Gitlab.config.omniauth.providers.find do |provider|
+ provider.name == name || (provider.name == 'openid_connect' && provider.args.name == name)
+ end
merge_provider_args_with_defaults!(provider)
provider
diff --git a/lib/gitlab/auth/o_auth/user.rb b/lib/gitlab/auth/o_auth/user.rb
index 7d9c4c0d7c1..1fed2b263da 100644
--- a/lib/gitlab/auth/o_auth/user.rb
+++ b/lib/gitlab/auth/o_auth/user.rb
@@ -240,11 +240,11 @@ module Gitlab
valid_username = Uniquify.new.string(valid_username) { |s| !NamespacePathValidator.valid_path?(s) }
{
- name: name.strip.presence || valid_username,
- username: valid_username,
- email: email,
- password: auth_hash.password,
- password_confirmation: auth_hash.password,
+ name: name.strip.presence || valid_username,
+ username: valid_username,
+ email: email,
+ password: auth_hash.password,
+ password_confirmation: auth_hash.password,
password_automatically_set: true
}
end
diff --git a/lib/gitlab/auth/otp/strategies/forti_authenticator/manual_otp.rb b/lib/gitlab/auth/otp/strategies/forti_authenticator/manual_otp.rb
index 9cf1b2247a7..88ad48c3db7 100644
--- a/lib/gitlab/auth/otp/strategies/forti_authenticator/manual_otp.rb
+++ b/lib/gitlab/auth/otp/strategies/forti_authenticator/manual_otp.rb
@@ -34,7 +34,7 @@ module Gitlab
end
def body
- { username: user.username,
+ { username: user.username,
token_code: @otp_code }
end
diff --git a/lib/gitlab/auth/user_access_denied_reason.rb b/lib/gitlab/auth/user_access_denied_reason.rb
index ff6dc7313bb..322dfa74d09 100644
--- a/lib/gitlab/auth/user_access_denied_reason.rb
+++ b/lib/gitlab/auth/user_access_denied_reason.rb
@@ -57,3 +57,5 @@ module Gitlab
end
end
end
+
+Gitlab::Auth::UserAccessDeniedReason.prepend_mod
diff --git a/lib/gitlab/background_migration/backfill_cluster_agents_has_vulnerabilities.rb b/lib/gitlab/background_migration/backfill_cluster_agents_has_vulnerabilities.rb
new file mode 100644
index 00000000000..2ee0594d0a6
--- /dev/null
+++ b/lib/gitlab/background_migration/backfill_cluster_agents_has_vulnerabilities.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # Backfills the `vulnerability_reads.casted_cluster_agent_id` column
+ class BackfillClusterAgentsHasVulnerabilities < Gitlab::BackgroundMigration::BatchedMigrationJob
+ VULNERABILITY_READS_JOIN = <<~SQL
+ INNER JOIN vulnerability_reads
+ ON vulnerability_reads.casted_cluster_agent_id = cluster_agents.id AND
+ vulnerability_reads.project_id = cluster_agents.project_id AND
+ vulnerability_reads.report_type = 7
+ SQL
+
+ RELATION = ->(relation) do
+ relation
+ .where(has_vulnerabilities: false)
+ end
+
+ def perform
+ each_sub_batch(
+ operation_name: :update_all,
+ batching_scope: RELATION
+ ) do |sub_batch|
+ sub_batch
+ .joins(VULNERABILITY_READS_JOIN)
+ .update_all(has_vulnerabilities: true)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/backfill_draft_status_on_merge_requests_with_corrected_regex.rb b/lib/gitlab/background_migration/backfill_draft_status_on_merge_requests_with_corrected_regex.rb
index b9151343d6a..2d64b7378be 100644
--- a/lib/gitlab/background_migration/backfill_draft_status_on_merge_requests_with_corrected_regex.rb
+++ b/lib/gitlab/background_migration/backfill_draft_status_on_merge_requests_with_corrected_regex.rb
@@ -9,6 +9,7 @@ module Gitlab
# Migration only version of MergeRequest table
class MergeRequest < ::ApplicationRecord
include EachBatch
+ validates :suggested_reviewers, json_schema: { filename: 'merge_request_suggested_reviewers' }
CORRECTED_REGEXP_STR = "^(\\[draft\\]|\\(draft\\)|draft:|draft|\\[WIP\\]|WIP:|WIP)"
diff --git a/lib/gitlab/background_migration/backfill_project_feature_package_registry_access_level.rb b/lib/gitlab/background_migration/backfill_project_feature_package_registry_access_level.rb
index 814f5a897a9..ce4c4a28b37 100644
--- a/lib/gitlab/background_migration/backfill_project_feature_package_registry_access_level.rb
+++ b/lib/gitlab/background_migration/backfill_project_feature_package_registry_access_level.rb
@@ -22,7 +22,7 @@ module Gitlab
ProjectFeature.connection.execute(
<<~SQL
UPDATE project_features pf
- SET package_registry_access_level = (CASE p.packages_enabled
+ SET package_registry_access_level = (CASE p.packages_enabled
WHEN true THEN (CASE p.visibility_level
WHEN #{PROJECT_PUBLIC} THEN #{FEATURE_PUBLIC}
WHEN #{PROJECT_INTERNAL} THEN #{FEATURE_ENABLED}
diff --git a/lib/gitlab/background_migration/backfill_project_namespace_on_issues.rb b/lib/gitlab/background_migration/backfill_project_namespace_on_issues.rb
new file mode 100644
index 00000000000..815c346bb39
--- /dev/null
+++ b/lib/gitlab/background_migration/backfill_project_namespace_on_issues.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # Back-fills the `issues.namespace_id` by setting it to corresponding project.project_namespace_id
+ class BackfillProjectNamespaceOnIssues < BatchedMigrationJob
+ def perform
+ each_sub_batch(
+ operation_name: :update_all,
+ batching_scope: -> (relation) {
+ relation.joins("INNER JOIN projects ON projects.id = issues.project_id")
+ .select("issues.id AS issue_id, projects.project_namespace_id").where(issues: { namespace_id: nil })
+ }
+ ) do |sub_batch|
+ connection.execute <<~SQL
+ UPDATE issues
+ SET namespace_id = projects.project_namespace_id
+ FROM (#{sub_batch.to_sql}) AS projects(issue_id, project_namespace_id)
+ WHERE issues.id = issue_id
+ SQL
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/backfill_project_repositories.rb b/lib/gitlab/background_migration/backfill_project_repositories.rb
index 05e2ed72fb3..c49ef9d10f5 100644
--- a/lib/gitlab/background_migration/backfill_project_repositories.rb
+++ b/lib/gitlab/background_migration/backfill_project_repositories.rb
@@ -212,8 +212,8 @@ module Gitlab
def build_attributes_for_project(project)
{
project_id: project.id,
- shard_id: find_shard_id(project.repository_storage),
- disk_path: project.disk_path
+ shard_id: find_shard_id(project.repository_storage),
+ disk_path: project.disk_path
}
end
diff --git a/lib/gitlab/background_migration/backfill_vulnerability_reads_cluster_agent.rb b/lib/gitlab/background_migration/backfill_vulnerability_reads_cluster_agent.rb
index 728b60f7a0e..0c41d6af209 100644
--- a/lib/gitlab/background_migration/backfill_vulnerability_reads_cluster_agent.rb
+++ b/lib/gitlab/background_migration/backfill_vulnerability_reads_cluster_agent.rb
@@ -10,16 +10,12 @@ module Gitlab
vulnerability_reads.project_id = cluster_agents.project_id
SQL
- RELATION = ->(relation) do
- relation
- .where(report_type: 7)
- end
+ CLUSTER_IMAGE_SCANNING_REPORT_TYPE = 7
+
+ scope_to ->(relation) { relation.where(report_type: CLUSTER_IMAGE_SCANNING_REPORT_TYPE) }
def perform
- each_sub_batch(
- operation_name: :update_all,
- batching_scope: RELATION
- ) do |sub_batch|
+ each_sub_batch(operation_name: :update_all) do |sub_batch|
sub_batch
.joins(CLUSTER_AGENTS_JOIN)
.update_all('casted_cluster_agent_id = CAST(vulnerability_reads.cluster_agent_id AS bigint)')
diff --git a/lib/gitlab/background_migration/backfill_work_item_type_id_for_issues.rb b/lib/gitlab/background_migration/backfill_work_item_type_id_for_issues.rb
index 32962f2bb89..86d53ad798d 100644
--- a/lib/gitlab/background_migration/backfill_work_item_type_id_for_issues.rb
+++ b/lib/gitlab/background_migration/backfill_work_item_type_id_for_issues.rb
@@ -4,11 +4,9 @@ module Gitlab
module BackgroundMigration
# Backfills the `issues.work_item_type_id` column, replacing any
# instances of `NULL` with the appropriate `work_item_types.id` based on `issues.issue_type`
- class BackfillWorkItemTypeIdForIssues
+ class BackfillWorkItemTypeIdForIssues < BatchedMigrationJob
# Basic AR model for issues table
class MigrationIssue < ApplicationRecord
- include ::EachBatch
-
self.table_name = 'issues'
scope :base_query, ->(base_type) { where(work_item_type_id: nil, issue_type: base_type) }
@@ -16,29 +14,27 @@ module Gitlab
MAX_UPDATE_RETRIES = 3
- def perform(start_id, end_id, batch_table, batch_column, sub_batch_size, pause_ms, base_type, base_type_id)
- parent_batch_relation = relation_scoped_to_range(batch_table, batch_column, start_id, end_id, base_type)
+ scope_to ->(relation) {
+ relation.where(issue_type: base_type)
+ }
+
+ job_arguments :base_type, :base_type_id
- parent_batch_relation.each_batch(column: batch_column, of: sub_batch_size) do |sub_batch|
+ def perform
+ each_sub_batch(
+ operation_name: :update_all,
+ batching_scope: -> (relation) { relation.where(work_item_type_id: nil) }
+ ) do |sub_batch|
first, last = sub_batch.pick(Arel.sql('min(id), max(id)'))
# The query need to be reconstructed because .each_batch modifies the default scope
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/330510
reconstructed_sub_batch = MigrationIssue.unscoped.base_query(base_type).where(id: first..last)
- batch_metrics.time_operation(:update_all) do
- update_with_retry(reconstructed_sub_batch, base_type_id)
- end
-
- pause_ms = 0 if pause_ms < 0
- sleep(pause_ms * 0.001)
+ update_with_retry(reconstructed_sub_batch, base_type_id)
end
end
- def batch_metrics
- @batch_metrics ||= Gitlab::Database::BackgroundMigration::BatchMetrics.new
- end
-
private
# Retry mechanism required as update statements on the issues table will randomly take longer than
@@ -64,10 +60,6 @@ module Gitlab
def update_batch(sub_batch, base_type_id)
sub_batch.update_all(work_item_type_id: base_type_id)
end
-
- def relation_scoped_to_range(source_table, source_key_column, start_id, end_id, base_type)
- MigrationIssue.where(source_key_column => start_id..end_id).base_query(base_type)
- end
end
end
end
diff --git a/lib/gitlab/background_migration/batching_strategies/backfill_issue_work_item_type_batching_strategy.rb b/lib/gitlab/background_migration/batching_strategies/backfill_issue_work_item_type_batching_strategy.rb
deleted file mode 100644
index 7d5fef67c25..00000000000
--- a/lib/gitlab/background_migration/batching_strategies/backfill_issue_work_item_type_batching_strategy.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module BackgroundMigration
- module BatchingStrategies
- # Batching class to use for back-filling issue's work_item_type_id for a single issue type.
- # Batches will be scoped to records where the foreign key is NULL and only of a given issue type
- #
- # If no more batches exist in the table, returns nil.
- class BackfillIssueWorkItemTypeBatchingStrategy < PrimaryKeyBatchingStrategy
- def apply_additional_filters(relation, job_arguments:, job_class: nil)
- issue_type = job_arguments.first
-
- relation.where(issue_type: issue_type)
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/background_migration/batching_strategies/backfill_project_statistics_with_container_registry_size_batching_strategy.rb b/lib/gitlab/background_migration/batching_strategies/backfill_project_statistics_with_container_registry_size_batching_strategy.rb
index 9ad119310f7..72da2b5a2b7 100644
--- a/lib/gitlab/background_migration/batching_strategies/backfill_project_statistics_with_container_registry_size_batching_strategy.rb
+++ b/lib/gitlab/background_migration/batching_strategies/backfill_project_statistics_with_container_registry_size_batching_strategy.rb
@@ -3,18 +3,9 @@
module Gitlab
module BackgroundMigration
module BatchingStrategies
- # Batching class to use for back-filling project_statistic's container_registry_size.
- # Batches will be scoped to records where the project_ids are migrated
- #
- # If no more batches exist in the table, returns nil.
+ # Used to apply additional filters to the batching table, migrated to
+ # use BatchedMigrationJob#filter_batch with https://gitlab.com/gitlab-org/gitlab/-/merge_requests/93771
class BackfillProjectStatisticsWithContainerRegistrySizeBatchingStrategy < PrimaryKeyBatchingStrategy
- MIGRATION_PHASE_1_ENDED_AT = Date.new(2022, 01, 23).freeze
-
- def apply_additional_filters(relation, job_arguments: [], job_class: nil)
- relation.where(created_at: MIGRATION_PHASE_1_ENDED_AT..).or(
- relation.where(migration_state: 'import_done')
- ).select(:project_id).distinct
- end
end
end
end
diff --git a/lib/gitlab/background_migration/batching_strategies/backfill_vulnerability_reads_cluster_agent_batching_strategy.rb b/lib/gitlab/background_migration/batching_strategies/backfill_vulnerability_reads_cluster_agent_batching_strategy.rb
index f0d015198dc..c2fa00f66de 100644
--- a/lib/gitlab/background_migration/batching_strategies/backfill_vulnerability_reads_cluster_agent_batching_strategy.rb
+++ b/lib/gitlab/background_migration/batching_strategies/backfill_vulnerability_reads_cluster_agent_batching_strategy.rb
@@ -3,16 +3,9 @@
module Gitlab
module BackgroundMigration
module BatchingStrategies
- # Batching class to use for back-filling vulnerability_read's casted_cluster_agent_id from cluster_agent_id.
- # Batches will be scoped to records where the report_type belongs to cluster_image_scanning.
- #
- # If no more batches exist in the table, returns nil.
+ # Used to apply additional filters to the batching table, migrated to
+ # use BatchedMigrationJob#filter_batch with https://gitlab.com/gitlab-org/gitlab/-/merge_requests/93771
class BackfillVulnerabilityReadsClusterAgentBatchingStrategy < PrimaryKeyBatchingStrategy
- CLUSTER_IMAGE_SCANNING_REPORT_TYPE = 7
-
- def apply_additional_filters(relation, job_arguments: [], job_class: nil)
- relation.where(report_type: CLUSTER_IMAGE_SCANNING_REPORT_TYPE)
- end
end
end
end
diff --git a/lib/gitlab/background_migration/batching_strategies/dismissed_vulnerabilities_strategy.rb b/lib/gitlab/background_migration/batching_strategies/dismissed_vulnerabilities_strategy.rb
index e1855b6cfee..9504d4eec11 100644
--- a/lib/gitlab/background_migration/batching_strategies/dismissed_vulnerabilities_strategy.rb
+++ b/lib/gitlab/background_migration/batching_strategies/dismissed_vulnerabilities_strategy.rb
@@ -3,14 +3,9 @@
module Gitlab
module BackgroundMigration
module BatchingStrategies
- # Batching class to use for setting state in vulnerabilitites table.
- # Batches will be scoped to records where the dismissed_at is set.
- #
- # If no more batches exist in the table, returns nil.
+ # Used to apply additional filters to the batching table, migrated to
+ # use BatchedMigrationJob#filter_batch with https://gitlab.com/gitlab-org/gitlab/-/merge_requests/93771
class DismissedVulnerabilitiesStrategy < PrimaryKeyBatchingStrategy
- def apply_additional_filters(relation, job_arguments: [], job_class: nil)
- relation.where.not(dismissed_at: nil)
- end
end
end
end
diff --git a/lib/gitlab/background_migration/batching_strategies/primary_key_batching_strategy.rb b/lib/gitlab/background_migration/batching_strategies/primary_key_batching_strategy.rb
index 1ffa4a052e5..43352b1bf91 100644
--- a/lib/gitlab/background_migration/batching_strategies/primary_key_batching_strategy.rb
+++ b/lib/gitlab/background_migration/batching_strategies/primary_key_batching_strategy.rb
@@ -22,8 +22,8 @@ module Gitlab
def next_batch(table_name, column_name, batch_min_value:, batch_size:, job_arguments:, job_class: nil)
model_class = define_batchable_model(table_name, connection: connection)
- quoted_column_name = model_class.connection.quote_column_name(column_name)
- relation = model_class.where("#{quoted_column_name} >= ?", batch_min_value)
+ arel_column = model_class.arel_table[column_name]
+ relation = model_class.where(arel_column.gteq(batch_min_value))
if job_class
relation = filter_batch(relation,
@@ -32,11 +32,10 @@ module Gitlab
)
end
- relation = apply_additional_filters(relation, job_arguments: job_arguments, job_class: job_class)
next_batch_bounds = nil
relation.each_batch(of: batch_size, column: column_name) do |batch| # rubocop:disable Lint/UnreachableLoop
- next_batch_bounds = batch.pick(Arel.sql("MIN(#{quoted_column_name}), MAX(#{quoted_column_name})"))
+ next_batch_bounds = batch.pick(arel_column.minimum, arel_column.maximum)
break
end
@@ -44,15 +43,6 @@ module Gitlab
next_batch_bounds
end
- # Deprecated
- #
- # Use `scope_to` to define additional filters on the migration job class.
- #
- # see https://docs.gitlab.com/ee/development/database/batched_background_migrations.html#adding-additional-filters.
- def apply_additional_filters(relation, job_arguments: [], job_class: nil)
- relation
- end
-
private
def filter_batch(relation, table_name:, column_name:, job_class:, job_arguments: [])
diff --git a/lib/gitlab/background_migration/batching_strategies/remove_backfilled_job_artifacts_expire_at_batching_strategy.rb b/lib/gitlab/background_migration/batching_strategies/remove_backfilled_job_artifacts_expire_at_batching_strategy.rb
new file mode 100644
index 00000000000..49525479637
--- /dev/null
+++ b/lib/gitlab/background_migration/batching_strategies/remove_backfilled_job_artifacts_expire_at_batching_strategy.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ module BatchingStrategies
+ # Used to apply additional filters to the batching table, migrated to
+ # use BatchedMigrationJob#filter_batch with https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96478
+ class RemoveBackfilledJobArtifactsExpireAtBatchingStrategy < PrimaryKeyBatchingStrategy
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/delete_approval_rules_with_vulnerability.rb b/lib/gitlab/background_migration/delete_approval_rules_with_vulnerability.rb
new file mode 100644
index 00000000000..739197898d9
--- /dev/null
+++ b/lib/gitlab/background_migration/delete_approval_rules_with_vulnerability.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # This class doesn't delete approval rules
+ # as this feature exists only in EE
+ class DeleteApprovalRulesWithVulnerability < BatchedMigrationJob
+ def perform
+ end
+ end
+ end
+end
+
+# rubocop:disable Layout/LineLength
+Gitlab::BackgroundMigration::DeleteApprovalRulesWithVulnerability.prepend_mod_with('Gitlab::BackgroundMigration::DeleteApprovalRulesWithVulnerability')
+# rubocop:enable Layout/LineLength
diff --git a/lib/gitlab/background_migration/destroy_invalid_group_members.rb b/lib/gitlab/background_migration/destroy_invalid_group_members.rb
new file mode 100644
index 00000000000..35ac42f76ab
--- /dev/null
+++ b/lib/gitlab/background_migration/destroy_invalid_group_members.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ class DestroyInvalidGroupMembers < Gitlab::BackgroundMigration::BatchedMigrationJob # rubocop:disable Style/Documentation
+ scope_to ->(relation) do
+ relation.where(source_type: 'Namespace')
+ .joins('LEFT OUTER JOIN namespaces ON members.source_id = namespaces.id')
+ .where(namespaces: { id: nil })
+ end
+
+ def perform
+ each_sub_batch(operation_name: :delete_all) do |sub_batch|
+ invalid_ids = sub_batch.map(&:id)
+ Gitlab::AppLogger.info({ message: 'Removing invalid group member records',
+ deleted_count: invalid_ids.size, ids: invalid_ids })
+
+ sub_batch.delete_all
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/destroy_invalid_project_members.rb b/lib/gitlab/background_migration/destroy_invalid_project_members.rb
new file mode 100644
index 00000000000..3c60f765c29
--- /dev/null
+++ b/lib/gitlab/background_migration/destroy_invalid_project_members.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ class DestroyInvalidProjectMembers < Gitlab::BackgroundMigration::BatchedMigrationJob # rubocop:disable Style/Documentation
+ scope_to ->(relation) { relation.where(source_type: 'Project') }
+
+ def perform
+ each_sub_batch(operation_name: :delete_all) do |sub_batch|
+ invalid_project_members = sub_batch
+ .joins('LEFT OUTER JOIN projects ON members.source_id = projects.id')
+ .where(projects: { id: nil })
+ invalid_ids = invalid_project_members.pluck(:id)
+
+ # the actual delete
+ deleted_count = invalid_project_members.delete_all
+
+ Gitlab::AppLogger.info({ message: 'Removing invalid project member records',
+ deleted_count: deleted_count,
+ ids: invalid_ids })
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/disable_legacy_open_source_licence_for_recent_public_projects.rb b/lib/gitlab/background_migration/disable_legacy_open_source_licence_for_recent_public_projects.rb
new file mode 100644
index 00000000000..824054b31f2
--- /dev/null
+++ b/lib/gitlab/background_migration/disable_legacy_open_source_licence_for_recent_public_projects.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # Set `project_settings.legacy_open_source_license_available` to false for public projects created after 17/02/2022
+ class DisableLegacyOpenSourceLicenceForRecentPublicProjects < ::Gitlab::BackgroundMigration::BatchedMigrationJob
+ PUBLIC = 20
+ THRESHOLD_DATE = '2022-02-17 09:00:00'
+
+ # Migration only version of `project_settings` table
+ class ProjectSetting < ApplicationRecord
+ self.table_name = 'project_settings'
+ end
+
+ def perform
+ each_sub_batch(
+ operation_name: :disable_legacy_open_source_licence_for_recent_public_projects,
+ batching_scope: ->(relation) {
+ relation.where(visibility_level: PUBLIC).where('created_at >= ?', THRESHOLD_DATE)
+ }
+ ) do |sub_batch|
+ ProjectSetting.where(project_id: sub_batch)
+ .where(legacy_open_source_license_available: true)
+ .update_all(legacy_open_source_license_available: false)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/disable_legacy_open_source_license_for_projects_less_than_one_mb.rb b/lib/gitlab/background_migration/disable_legacy_open_source_license_for_projects_less_than_one_mb.rb
new file mode 100644
index 00000000000..6e4d5d8ddcb
--- /dev/null
+++ b/lib/gitlab/background_migration/disable_legacy_open_source_license_for_projects_less_than_one_mb.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # Set `project_settings.legacy_open_source_license_available` to false for projects less than 1 MB
+ class DisableLegacyOpenSourceLicenseForProjectsLessThanOneMb < ::Gitlab::BackgroundMigration::BatchedMigrationJob
+ scope_to ->(relation) { relation.where(legacy_open_source_license_available: true) }
+
+ def perform
+ each_sub_batch(operation_name: :disable_legacy_open_source_license_for_projects_less_than_one_mb) do |sub_batch|
+ updates = { legacy_open_source_license_available: false, updated_at: Time.current }
+
+ sub_batch
+ .joins('INNER JOIN project_statistics ON project_statistics.project_id = project_settings.project_id')
+ .where('project_statistics.repository_size < ?', 1.megabyte)
+ .update_all(updates)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/mailers/unconfirm_mailer.rb b/lib/gitlab/background_migration/mailers/unconfirm_mailer.rb
index 3605b157f4f..2bf631c6c7d 100644
--- a/lib/gitlab/background_migration/mailers/unconfirm_mailer.rb
+++ b/lib/gitlab/background_migration/mailers/unconfirm_mailer.rb
@@ -11,7 +11,7 @@ module Gitlab
@user = user
@verification_from_mail = Gitlab.config.gitlab.email_from
- mail(
+ mail_with_locale(
template_path: 'unconfirm_mailer',
template_name: 'unconfirm_notification_email',
to: @user.notification_email_or_default,
diff --git a/lib/gitlab/background_migration/recalculate_vulnerabilities_occurrences_uuid.rb b/lib/gitlab/background_migration/recalculate_vulnerabilities_occurrences_uuid.rb
index 72380af2c53..9a42d035285 100644
--- a/lib/gitlab/background_migration/recalculate_vulnerabilities_occurrences_uuid.rb
+++ b/lib/gitlab/background_migration/recalculate_vulnerabilities_occurrences_uuid.rb
@@ -58,7 +58,7 @@ class Gitlab::BackgroundMigration::RecalculateVulnerabilitiesOccurrencesUuid # r
development: "a143e9e2-41b3-47bc-9a19-081d089229f4",
test: "a143e9e2-41b3-47bc-9a19-081d089229f4",
staging: "a6930898-a1b2-4365-ab18-12aa474d9b26",
- production: "58dc0f06-936c-43b3-93bb-71693f1b6570"
+ production: "58dc0f06-936c-43b3-93bb-71693f1b6570"
}.freeze
NAMESPACE_REGEX = /(\h{8})-(\h{4})-(\h{4})-(\h{4})-(\h{4})(\h{8})/.freeze
diff --git a/lib/gitlab/background_migration/remove_backfilled_job_artifacts_expire_at.rb b/lib/gitlab/background_migration/remove_backfilled_job_artifacts_expire_at.rb
new file mode 100644
index 00000000000..d30263976e8
--- /dev/null
+++ b/lib/gitlab/background_migration/remove_backfilled_job_artifacts_expire_at.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # This detects and fixes job artifacts that have `expire_at` wrongly backfilled by the migration
+ # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/47723.
+ # These job artifacts will not be deleted and will have their `expire_at` removed.
+ class RemoveBackfilledJobArtifactsExpireAt < BatchedMigrationJob
+ # The migration would have backfilled `expire_at`
+ # to midnight on the 22nd of the month of the local timezone,
+ # storing it as UTC time in the database.
+ #
+ # If the timezone setting has changed since the migration,
+ # the `expire_at` stored in the database could have changed to a different local time other than midnight.
+ # For example:
+ # - changing timezone from UTC+02:00 to UTC+02:30 would change the `expire_at` in local time 00:00:00 to 00:30:00.
+ # - changing timezone from UTC+00:00 to UTC-01:00 would change the `expire_at` in local time 00:00:00 to 23:00:00
+ # on the previous day (21st).
+ #
+ # Therefore job artifacts that have `expire_at` exactly on the 00, 30 or 45 minute mark
+ # on the dates 21, 22, 23 of the month will not be deleted.
+ # https://en.wikipedia.org/wiki/List_of_UTC_time_offsets
+ EXPIRES_ON_21_22_23_AT_MIDNIGHT_IN_TIMEZONE = <<~SQL
+ EXTRACT(day FROM timezone('UTC', expire_at)) IN (21, 22, 23)
+ AND EXTRACT(minute FROM timezone('UTC', expire_at)) IN (0, 30, 45)
+ AND EXTRACT(second FROM timezone('UTC', expire_at)) = 0
+ SQL
+
+ scope_to ->(relation) {
+ relation.where(EXPIRES_ON_21_22_23_AT_MIDNIGHT_IN_TIMEZONE)
+ .or(relation.where(file_type: 3))
+ }
+
+ def perform
+ each_sub_batch(
+ operation_name: :update_all
+ ) do |sub_batch|
+ sub_batch.update_all(expire_at: nil)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/remove_self_managed_wiki_notes.rb b/lib/gitlab/background_migration/remove_self_managed_wiki_notes.rb
new file mode 100644
index 00000000000..5b1d630bb03
--- /dev/null
+++ b/lib/gitlab/background_migration/remove_self_managed_wiki_notes.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # Removes obsolete wiki notes
+ class RemoveSelfManagedWikiNotes < BatchedMigrationJob
+ def perform
+ each_sub_batch(
+ operation_name: :delete_all
+ ) do |sub_batch|
+ sub_batch.where(noteable_type: 'Wiki').delete_all
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/rename_task_system_note_to_checklist_item.rb b/lib/gitlab/background_migration/rename_task_system_note_to_checklist_item.rb
new file mode 100644
index 00000000000..718fb0aaa71
--- /dev/null
+++ b/lib/gitlab/background_migration/rename_task_system_note_to_checklist_item.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # Renames all system notes created when an issuable task is checked/unchecked
+ # from `task` into `checklist item`
+ # `marked the task **Task 1** as incomplete` => `marked the checklist item **Task 1** as incomplete`
+ class RenameTaskSystemNoteToChecklistItem < BatchedMigrationJob
+ REPLACE_REGEX = '\Amarked\sthe\stask'
+ TEXT_REPLACEMENT = 'marked the checklist item'
+
+ scope_to ->(relation) {
+ relation.where(system_note_metadata: { action: :task })
+ }
+
+ def perform
+ each_sub_batch(operation_name: :update_all) do |sub_batch|
+ ApplicationRecord.connection.execute <<~SQL
+ UPDATE notes
+ SET note = REGEXP_REPLACE(notes.note,'#{REPLACE_REGEX}', '#{TEXT_REPLACEMENT}')
+ FROM (#{sub_batch.select(:note_id).to_sql}) AS metadata_fields(note_id)
+ WHERE notes.id = note_id
+ SQL
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/set_correct_vulnerability_state.rb b/lib/gitlab/background_migration/set_correct_vulnerability_state.rb
index fd6cbcb8d05..a0cfeed618a 100644
--- a/lib/gitlab/background_migration/set_correct_vulnerability_state.rb
+++ b/lib/gitlab/background_migration/set_correct_vulnerability_state.rb
@@ -6,11 +6,10 @@ module Gitlab
class SetCorrectVulnerabilityState < BatchedMigrationJob
DISMISSED_STATE = 2
+ scope_to ->(relation) { relation.where.not(dismissed_at: nil) }
+
def perform
- each_sub_batch(
- operation_name: :update_vulnerabilities_state,
- batching_scope: -> (relation) { relation.where.not(dismissed_at: nil) }
- ) do |sub_batch|
+ each_sub_batch(operation_name: :update_vulnerabilities_state) do |sub_batch|
sub_batch.update_all(state: DISMISSED_STATE)
end
end
diff --git a/lib/gitlab/base_doorkeeper_controller.rb b/lib/gitlab/base_doorkeeper_controller.rb
index 81b01395542..c8520993b8e 100644
--- a/lib/gitlab/base_doorkeeper_controller.rb
+++ b/lib/gitlab/base_doorkeeper_controller.rb
@@ -3,6 +3,7 @@
# This is a base controller for doorkeeper.
# It adds the `can?` helper used in the views.
module Gitlab
+ # rubocop:disable Rails/ApplicationController
class BaseDoorkeeperController < ActionController::Base
include Gitlab::Allowable
include EnforcesTwoFactorAuthentication
@@ -12,4 +13,5 @@ module Gitlab
helper_method :can?
end
+ # rubocop:enable Rails/ApplicationController
end
diff --git a/lib/gitlab/cache/helpers.rb b/lib/gitlab/cache/helpers.rb
index 7b11d6bc9ff..48b6ca59367 100644
--- a/lib/gitlab/cache/helpers.rb
+++ b/lib/gitlab/cache/helpers.rb
@@ -57,9 +57,19 @@ module Gitlab
# @param expires_in [ActiveSupport::Duration, Integer] an expiry time for the cache entry
# @return [String]
def cached_object(object, presenter:, presenter_args:, context:, expires_in:)
- cache.fetch(contextual_cache_key(presenter, object, context), expires_in: expires_in) do
- Gitlab::Json.dump(presenter.represent(object, **presenter_args).as_json)
+ misses = 0
+
+ json = cache.fetch(contextual_cache_key(presenter, object, context), expires_in: expires_in) do
+ time_action(render_type: :object) do
+ misses += 1
+
+ Gitlab::Json.dump(presenter.represent(object, **presenter_args).as_json)
+ end
end
+
+ increment_cache_metric(render_type: :object, total_count: 1, miss_count: misses)
+
+ json
end
# Used for fetching or rendering multiple objects
@@ -71,10 +81,18 @@ module Gitlab
# @param expires_in [ActiveSupport::Duration, Integer] an expiry time for the cache entry
# @return [Array<String>]
def cached_collection(collection, presenter:, presenter_args:, context:, expires_in:)
+ misses = 0
+
json = fetch_multi(presenter, collection, context: context, expires_in: expires_in) do |obj|
- Gitlab::Json.dump(presenter.represent(obj, **presenter_args).as_json)
+ time_action(render_type: :collection) do
+ misses += 1
+
+ Gitlab::Json.dump(presenter.represent(obj, **presenter_args).as_json)
+ end
end
+ increment_cache_metric(render_type: :collection, total_count: collection.length, miss_count: misses)
+
json.values
end
@@ -106,6 +124,57 @@ module Gitlab
contextual_cache_key(presenter, object, context)
end
end
+
+ def increment_cache_metric(render_type:, total_count:, miss_count:)
+ return unless Feature.enabled?(:add_timing_to_certain_cache_actions)
+ return unless caller_id
+
+ metric_name = :cached_object_operations_total
+ hit_count = total_count - miss_count
+
+ current_transaction&.increment(
+ metric_name,
+ hit_count,
+ { caller_id: caller_id, render_type: render_type, cache_hit: true }
+ )
+
+ current_transaction&.increment(
+ metric_name,
+ miss_count,
+ { caller_id: caller_id, render_type: render_type, cache_hit: false }
+ )
+ end
+
+ def time_action(render_type:, &block)
+ if Feature.enabled?(:add_timing_to_certain_cache_actions)
+ real_start = Gitlab::Metrics::System.monotonic_time
+
+ presented_object = yield
+
+ real_duration_histogram(render_type).observe({}, Gitlab::Metrics::System.monotonic_time - real_start)
+
+ presented_object
+ else
+ yield
+ end
+ end
+
+ def real_duration_histogram(render_type)
+ Gitlab::Metrics.histogram(
+ :gitlab_presentable_object_cacheless_render_real_duration_seconds,
+ 'Duration of generating presentable objects to be cached in real time',
+ { caller_id: caller_id, render_type: render_type },
+ [0.1, 0.5, 1, 2]
+ )
+ end
+
+ def current_transaction
+ @current_transaction ||= ::Gitlab::Metrics::WebTransaction.current
+ end
+
+ def caller_id
+ @caller_id ||= Gitlab::ApplicationContext.current_context_attribute(:caller_id)
+ end
end
end
end
diff --git a/lib/gitlab/ci/ansi2html.rb b/lib/gitlab/ci/ansi2html.rb
index 10233cf4228..2ab702aa4f9 100644
--- a/lib/gitlab/ci/ansi2html.rb
+++ b/lib/gitlab/ci/ansi2html.rb
@@ -19,11 +19,11 @@ module Gitlab
}.freeze
STYLE_SWITCHES = {
- bold: 0x01,
- italic: 0x02,
- underline: 0x04,
- conceal: 0x08,
- cross: 0x10
+ bold: 0x01,
+ italic: 0x02,
+ underline: 0x04,
+ conceal: 0x08,
+ cross: 0x10
}.freeze
def self.convert(ansi, state = nil)
diff --git a/lib/gitlab/ci/ansi2json/parser.rb b/lib/gitlab/ci/ansi2json/parser.rb
index 79b42a5f5bf..fdd49df1e24 100644
--- a/lib/gitlab/ci/ansi2json/parser.rb
+++ b/lib/gitlab/ci/ansi2json/parser.rb
@@ -20,11 +20,11 @@ module Gitlab
}.freeze
STYLE_SWITCHES = {
- bold: 0x01,
- italic: 0x02,
- underline: 0x04,
- conceal: 0x08,
- cross: 0x10
+ bold: 0x01,
+ italic: 0x02,
+ underline: 0x04,
+ conceal: 0x08,
+ cross: 0x10
}.freeze
def self.bold?(mask)
diff --git a/lib/gitlab/ci/build/artifacts/adapters/zip_stream.rb b/lib/gitlab/ci/build/artifacts/adapters/zip_stream.rb
deleted file mode 100644
index 690a47097c6..00000000000
--- a/lib/gitlab/ci/build/artifacts/adapters/zip_stream.rb
+++ /dev/null
@@ -1,61 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Ci
- module Build
- module Artifacts
- module Adapters
- class ZipStream
- MAX_DECOMPRESSED_SIZE = 100.megabytes
- MAX_FILES_PROCESSED = 50
-
- attr_reader :stream
-
- InvalidStreamError = Class.new(StandardError)
-
- def initialize(stream)
- raise InvalidStreamError, "Stream is required" unless stream
-
- @stream = stream
- @files_processed = 0
- end
-
- def each_blob
- Zip::InputStream.open(stream) do |zio|
- while entry = zio.get_next_entry
- break if at_files_processed_limit?
- next unless should_process?(entry)
-
- @files_processed += 1
-
- yield entry.get_input_stream.read
- end
- end
- end
-
- private
-
- def should_process?(entry)
- file?(entry) && !too_large?(entry)
- end
-
- def file?(entry)
- # Check the file name as a workaround for incorrect
- # file type detection when using InputStream
- # https://github.com/rubyzip/rubyzip/issues/533
- entry.file? && !entry.name.end_with?('/')
- end
-
- def too_large?(entry)
- entry.size > MAX_DECOMPRESSED_SIZE
- end
-
- def at_files_processed_limit?
- @files_processed >= MAX_FILES_PROCESSED
- end
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/ci/build/context/build.rb b/lib/gitlab/ci/build/context/build.rb
index 641aa71fb4e..a1a8e9288c7 100644
--- a/lib/gitlab/ci/build/context/build.rb
+++ b/lib/gitlab/ci/build/context/build.rb
@@ -32,7 +32,18 @@ module Gitlab
end
def build_attributes
- attributes.merge(pipeline_attributes)
+ attributes.merge(pipeline_attributes, ci_stage_attributes)
+ end
+
+ def ci_stage_attributes
+ {
+ ci_stage: ::Ci::Stage.new(
+ name: attributes[:stage],
+ position: attributes[:stage_idx],
+ pipeline: pipeline_attributes[:pipeline],
+ project: pipeline_attributes[:project]
+ )
+ }
end
end
end
diff --git a/lib/gitlab/ci/build/rules/rule/clause/exists.rb b/lib/gitlab/ci/build/rules/rule/clause/exists.rb
index e2b54797dc8..aebd81e7b07 100644
--- a/lib/gitlab/ci/build/rules/rule/clause/exists.rb
+++ b/lib/gitlab/ci/build/rules/rule/clause/exists.rb
@@ -24,7 +24,7 @@ module Gitlab
private
def worktree_paths(context)
- return unless context.project
+ return [] unless context.project
if @top_level_only
context.top_level_worktree_paths
diff --git a/lib/gitlab/ci/config/entry/current_variables.rb b/lib/gitlab/ci/config/entry/current_variables.rb
new file mode 100644
index 00000000000..3b6721ec92d
--- /dev/null
+++ b/lib/gitlab/ci/config/entry/current_variables.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ class Config
+ module Entry
+ ##
+ # Entry that represents CI/CD variables.
+ # The class will be renamed to `Variables` when removing the FF `ci_variables_refactoring_to_variable`.
+ #
+ class CurrentVariables < ::Gitlab::Config::Entry::ComposableHash
+ include ::Gitlab::Config::Entry::Validatable
+
+ validations do
+ validates :config, type: Hash
+ end
+
+ # Enable these lines when removing the FF `ci_variables_refactoring_to_variable`
+ # and renaming this class to `Variables`.
+ # def self.default(**)
+ # {}
+ # end
+
+ def value
+ @entries.to_h do |key, entry|
+ [key.to_s, entry.value]
+ end
+ end
+
+ def value_with_data
+ @entries.to_h do |key, entry|
+ [key.to_s, entry.value_with_data]
+ end
+ end
+
+ private
+
+ def composable_class(_name, _config)
+ Entry::Variable
+ end
+
+ def composable_metadata
+ { allowed_value_data: opt(:allowed_value_data) }
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/entry/environment.rb b/lib/gitlab/ci/config/entry/environment.rb
index 96ba3553b46..a727da87308 100644
--- a/lib/gitlab/ci/config/entry/environment.rb
+++ b/lib/gitlab/ci/config/entry/environment.rb
@@ -54,7 +54,7 @@ module Gitlab
validates :on_stop, type: String, allow_nil: true
validates :kubernetes, type: Hash, allow_nil: true
- validates :auto_stop_in, duration: { parser: ::Gitlab::Ci::Build::DurationParser }, allow_nil: true
+ validates :auto_stop_in, type: String, allow_nil: true
end
end
diff --git a/lib/gitlab/ci/config/entry/image.rb b/lib/gitlab/ci/config/entry/image.rb
index 613f7ff3370..84e31ca1fc6 100644
--- a/lib/gitlab/ci/config/entry/image.rb
+++ b/lib/gitlab/ci/config/entry/image.rb
@@ -11,10 +11,7 @@ module Gitlab
include ::Gitlab::Ci::Config::Entry::Imageable
validations do
- validates :config, allowed_keys: IMAGEABLE_ALLOWED_KEYS,
- if: :ci_docker_image_pull_policy_enabled?
- validates :config, allowed_keys: IMAGEABLE_LEGACY_ALLOWED_KEYS,
- unless: :ci_docker_image_pull_policy_enabled?
+ validates :config, allowed_keys: IMAGEABLE_ALLOWED_KEYS
end
def value
@@ -25,7 +22,7 @@ module Gitlab
name: @config[:name],
entrypoint: @config[:entrypoint],
ports: (ports_value if ports_defined?),
- pull_policy: (ci_docker_image_pull_policy_enabled? ? pull_policy_value : nil)
+ pull_policy: pull_policy_value
}.compact
else
{}
diff --git a/lib/gitlab/ci/config/entry/imageable.rb b/lib/gitlab/ci/config/entry/imageable.rb
index f045ee3d549..1aecfee9ab9 100644
--- a/lib/gitlab/ci/config/entry/imageable.rb
+++ b/lib/gitlab/ci/config/entry/imageable.rb
@@ -13,7 +13,6 @@ module Gitlab
include ::Gitlab::Config::Entry::Configurable
IMAGEABLE_ALLOWED_KEYS = %i[name entrypoint ports pull_policy].freeze
- IMAGEABLE_LEGACY_ALLOWED_KEYS = %i[name entrypoint ports].freeze
included do
include ::Gitlab::Config::Entry::Validatable
@@ -47,10 +46,6 @@ module Gitlab
opt(:with_image_ports)
end
- def ci_docker_image_pull_policy_enabled?
- ::Feature.enabled?(:ci_docker_image_pull_policy)
- end
-
def skip_config_hash_validation?
true
end
diff --git a/lib/gitlab/ci/config/entry/legacy_variables.rb b/lib/gitlab/ci/config/entry/legacy_variables.rb
new file mode 100644
index 00000000000..5379f707537
--- /dev/null
+++ b/lib/gitlab/ci/config/entry/legacy_variables.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ class Config
+ module Entry
+ ##
+ # Entry that represents environment variables.
+ # This is legacy implementation and will be removed with the FF `ci_variables_refactoring_to_variable`.
+ #
+ class LegacyVariables < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Validatable
+
+ ALLOWED_VALUE_DATA = %i[value description].freeze
+
+ validations do
+ validates :config, variables: { allowed_value_data: ALLOWED_VALUE_DATA }, if: :use_value_data?
+ validates :config, variables: true, unless: :use_value_data?
+ end
+
+ def value
+ @config.to_h { |key, value| [key.to_s, expand_value(value)[:value]] }
+ end
+
+ def value_with_data
+ @config.to_h { |key, value| [key.to_s, expand_value(value)] }
+ end
+
+ def use_value_data?
+ opt(:use_value_data)
+ end
+
+ private
+
+ def expand_value(value)
+ if value.is_a?(Hash)
+ { value: value[:value].to_s, description: value[:description] }.compact
+ else
+ { value: value.to_s }
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/entry/processable.rb b/lib/gitlab/ci/config/entry/processable.rb
index 78794f524f4..2d2032b1d8c 100644
--- a/lib/gitlab/ci/config/entry/processable.rb
+++ b/lib/gitlab/ci/config/entry/processable.rb
@@ -29,7 +29,7 @@ module Gitlab
in: %i[only except start_in],
message: 'key may not be used with `rules`'
},
- if: :has_rules?
+ if: :has_rules?
with_options allow_nil: true do
validates :extends, array_of_strings_or_string: true
@@ -120,7 +120,7 @@ module Gitlab
stage: stage_value,
extends: extends,
rules: rules_value,
- job_variables: variables_value.to_h,
+ job_variables: variables_entry.value_with_data,
root_variables_inheritance: root_variables_inheritance,
only: only_value,
except: except_value,
diff --git a/lib/gitlab/ci/config/entry/root.rb b/lib/gitlab/ci/config/entry/root.rb
index ff11c757dfa..57e89bd7bc5 100644
--- a/lib/gitlab/ci/config/entry/root.rb
+++ b/lib/gitlab/ci/config/entry/root.rb
@@ -48,9 +48,10 @@ module Gitlab
description: 'Script that will be executed after each job.',
reserved: true
+ # use_value_data will be removed with the FF ci_variables_refactoring_to_variable
entry :variables, Entry::Variables,
description: 'Environment variables that will be used.',
- metadata: { use_value_data: true },
+ metadata: { use_value_data: true, allowed_value_data: %i[value description] },
reserved: true
entry :stages, Entry::Stages,
diff --git a/lib/gitlab/ci/config/entry/service.rb b/lib/gitlab/ci/config/entry/service.rb
index 0e19447dff8..4b3a9990df4 100644
--- a/lib/gitlab/ci/config/entry/service.rb
+++ b/lib/gitlab/ci/config/entry/service.rb
@@ -11,14 +11,9 @@ module Gitlab
include ::Gitlab::Ci::Config::Entry::Imageable
ALLOWED_KEYS = %i[command alias variables].freeze
- LEGACY_ALLOWED_KEYS = %i[command alias variables].freeze
validations do
- validates :config, allowed_keys: ALLOWED_KEYS + IMAGEABLE_ALLOWED_KEYS,
- if: :ci_docker_image_pull_policy_enabled?
- validates :config, allowed_keys: LEGACY_ALLOWED_KEYS + IMAGEABLE_LEGACY_ALLOWED_KEYS,
- unless: :ci_docker_image_pull_policy_enabled?
-
+ validates :config, allowed_keys: ALLOWED_KEYS + IMAGEABLE_ALLOWED_KEYS
validates :command, array_of_strings: true, allow_nil: true
validates :alias, type: String, allow_nil: true
validates :alias, type: String, presence: true, unless: ->(record) { record.ports.blank? }
@@ -43,7 +38,7 @@ module Gitlab
{ name: @config }
elsif hash?
@config.merge(
- pull_policy: (pull_policy_value if ci_docker_image_pull_policy_enabled?)
+ pull_policy: pull_policy_value
).compact
else
{}
diff --git a/lib/gitlab/ci/config/entry/variable.rb b/lib/gitlab/ci/config/entry/variable.rb
new file mode 100644
index 00000000000..253888aadeb
--- /dev/null
+++ b/lib/gitlab/ci/config/entry/variable.rb
@@ -0,0 +1,98 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ class Config
+ module Entry
+ ##
+ # Entry that represents a CI/CD variable.
+ #
+ class Variable < ::Gitlab::Config::Entry::Simplifiable
+ strategy :SimpleVariable, if: -> (config) { SimpleVariable.applies_to?(config) }
+ strategy :ComplexVariable, if: -> (config) { ComplexVariable.applies_to?(config) }
+
+ class SimpleVariable < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Validatable
+
+ class << self
+ def applies_to?(config)
+ Gitlab::Config::Entry::Validators::AlphanumericValidator.validate(config)
+ end
+ end
+
+ validations do
+ validates :key, alphanumeric: true
+ validates :config, alphanumeric: true
+ end
+
+ def value
+ @config.to_s
+ end
+
+ def value_with_data
+ { value: @config.to_s }
+ end
+ end
+
+ class ComplexVariable < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Validatable
+
+ class << self
+ def applies_to?(config)
+ config.is_a?(Hash)
+ end
+ end
+
+ validations do
+ validates :key, alphanumeric: true
+ validates :config_value, alphanumeric: true, allow_nil: false, if: :config_value_defined?
+ validates :config_description, alphanumeric: true, allow_nil: false, if: :config_description_defined?
+
+ validate do
+ allowed_value_data = Array(opt(:allowed_value_data))
+
+ if allowed_value_data.any?
+ extra_keys = config.keys - allowed_value_data
+
+ errors.add(:config, "uses invalid data keys: #{extra_keys.join(', ')}") if extra_keys.present?
+ else
+ errors.add(:config, "must be a string")
+ end
+ end
+ end
+
+ def value
+ config_value.to_s
+ end
+
+ def value_with_data
+ { value: value, description: config_description }.compact
+ end
+
+ def config_value
+ @config[:value]
+ end
+
+ def config_description
+ @config[:description]
+ end
+
+ def config_value_defined?
+ config.key?(:value)
+ end
+
+ def config_description_defined?
+ config.key?(:description)
+ end
+ end
+
+ class UnknownStrategy < ::Gitlab::Config::Entry::Node
+ def errors
+ ["variable definition must be either a string or a hash"]
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/entry/variables.rb b/lib/gitlab/ci/config/entry/variables.rb
index efb469ee32a..0284958d9d4 100644
--- a/lib/gitlab/ci/config/entry/variables.rb
+++ b/lib/gitlab/ci/config/entry/variables.rb
@@ -5,43 +5,21 @@ module Gitlab
class Config
module Entry
##
- # Entry that represents environment variables.
+ # Entry that represents CI/CD variables.
+ # CurrentVariables will be renamed to this class when removing the FF `ci_variables_refactoring_to_variable`.
#
- class Variables < ::Gitlab::Config::Entry::Node
- include ::Gitlab::Config::Entry::Validatable
-
- ALLOWED_VALUE_DATA = %i[value description].freeze
-
- validations do
- validates :config, variables: { allowed_value_data: ALLOWED_VALUE_DATA }, if: :use_value_data?
- validates :config, variables: true, unless: :use_value_data?
- end
-
- def value
- @config.to_h { |key, value| [key.to_s, expand_value(value)[:value]] }
+ class Variables
+ def self.new(...)
+ if YamlProcessor::FeatureFlags.enabled?(:ci_variables_refactoring_to_variable)
+ CurrentVariables.new(...)
+ else
+ LegacyVariables.new(...)
+ end
end
def self.default(**)
{}
end
-
- def value_with_data
- @config.to_h { |key, value| [key.to_s, expand_value(value)] }
- end
-
- def use_value_data?
- opt(:use_value_data)
- end
-
- private
-
- def expand_value(value)
- if value.is_a?(Hash)
- { value: value[:value].to_s, description: value[:description] }
- else
- { value: value.to_s, description: nil }
- end
- end
end
end
end
diff --git a/lib/gitlab/ci/jwt_v2.rb b/lib/gitlab/ci/jwt_v2.rb
index 278353220e4..4e01688a955 100644
--- a/lib/gitlab/ci/jwt_v2.rb
+++ b/lib/gitlab/ci/jwt_v2.rb
@@ -8,7 +8,7 @@ module Gitlab
def reserved_claims
super.merge(
iss: Settings.gitlab.base_url,
- sub: "project_path:#{project.full_path}:ref_type:#{ref_type}:ref:#{source_ref}",
+ sub: "project_path:#{project.full_path}:ref_type:#{ref_type}:ref:#{source_ref}",
aud: Settings.gitlab.base_url
)
end
diff --git a/lib/gitlab/ci/parsers/sbom/cyclonedx.rb b/lib/gitlab/ci/parsers/sbom/cyclonedx.rb
index deb20a2138c..aa594ca4049 100644
--- a/lib/gitlab/ci/parsers/sbom/cyclonedx.rb
+++ b/lib/gitlab/ci/parsers/sbom/cyclonedx.rb
@@ -6,7 +6,6 @@ module Gitlab
module Sbom
class Cyclonedx
SUPPORTED_SPEC_VERSIONS = %w[1.4].freeze
- COMPONENT_ATTRIBUTES = %w[type name version].freeze
def parse!(blob, sbom_report)
@report = sbom_report
@@ -62,10 +61,17 @@ module Gitlab
end
def parse_components
- data['components']&.each do |component|
- next unless supported_component_type?(component['type'])
+ data['components']&.each do |component_data|
+ type = component_data['type']
+ next unless supported_component_type?(type)
- report.add_component(component.slice(*COMPONENT_ATTRIBUTES))
+ component = ::Gitlab::Ci::Reports::Sbom::Component.new(
+ type: type,
+ name: component_data['name'],
+ version: component_data['version']
+ )
+
+ report.add_component(component)
end
end
diff --git a/lib/gitlab/ci/parsers/sbom/source/dependency_scanning.rb b/lib/gitlab/ci/parsers/sbom/source/dependency_scanning.rb
index ad04b3257f9..00ca723b258 100644
--- a/lib/gitlab/ci/parsers/sbom/source/dependency_scanning.rb
+++ b/lib/gitlab/ci/parsers/sbom/source/dependency_scanning.rb
@@ -21,11 +21,11 @@ module Gitlab
def source
return unless required_attributes_present?
- {
- 'type' => :dependency_scanning,
- 'data' => data,
- 'fingerprint' => fingerprint
- }
+ ::Gitlab::Ci::Reports::Sbom::Source.new(
+ type: :dependency_scanning,
+ data: data,
+ fingerprint: fingerprint
+ )
end
private
diff --git a/lib/gitlab/ci/parsers/security/common.rb b/lib/gitlab/ci/parsers/security/common.rb
index 13a159f3745..da7faaab6ff 100644
--- a/lib/gitlab/ci/parsers/security/common.rb
+++ b/lib/gitlab/ci/parsers/security/common.rb
@@ -7,16 +7,16 @@ module Gitlab
class Common
SecurityReportParserError = Class.new(Gitlab::Ci::Parsers::ParserError)
- def self.parse!(json_data, report, vulnerability_finding_signatures_enabled = false, validate: false)
- new(json_data, report, vulnerability_finding_signatures_enabled, validate: validate).parse!
+ def self.parse!(json_data, report, signatures_enabled: false, validate: false)
+ new(json_data, report, signatures_enabled: signatures_enabled, validate: validate).parse!
end
- def initialize(json_data, report, vulnerability_finding_signatures_enabled = false, validate: false)
+ def initialize(json_data, report, signatures_enabled: false, validate: false)
@json_data = json_data
@report = report
@project = report.project
@validate = validate
- @vulnerability_finding_signatures_enabled = vulnerability_finding_signatures_enabled
+ @signatures_enabled = signatures_enabled
end
def parse!
@@ -26,7 +26,7 @@ module Gitlab
raise SecurityReportParserError, "Invalid report format" unless report_data.is_a?(Hash)
- create_scanner
+ create_scanner(top_level_scanner_data)
create_scan
create_analyzer
@@ -77,7 +77,7 @@ module Gitlab
report_data,
report.version,
project: @project,
- scanner: top_level_scanner
+ scanner: top_level_scanner_data
)
end
@@ -89,8 +89,8 @@ module Gitlab
@report_version ||= report_data['version']
end
- def top_level_scanner
- @top_level_scanner ||= report_data.dig('scan', 'scanner')
+ def top_level_scanner_data
+ @top_level_scanner_data ||= report_data.dig('scan', 'scanner')
end
def scan_data
@@ -119,7 +119,7 @@ module Gitlab
evidence = create_evidence(data['evidence'])
signatures = create_signatures(tracking_data(data))
- if @vulnerability_finding_signatures_enabled && !signatures.empty?
+ if @signatures_enabled && !signatures.empty?
# NOT the signature_sha - the compare key is hashed
# to create the project_fingerprint
highest_priority_signature = signatures.max_by(&:priority)
@@ -138,7 +138,7 @@ module Gitlab
evidence: evidence,
severity: parse_severity_level(data['severity']),
confidence: parse_confidence_level(data['confidence']),
- scanner: create_scanner(data['scanner']),
+ scanner: create_scanner(top_level_scanner_data || data['scanner']),
scan: report&.scan,
identifiers: identifiers,
flags: flags,
@@ -149,7 +149,7 @@ module Gitlab
details: data['details'] || {},
signatures: signatures,
project_id: @project.id,
- vulnerability_finding_signatures_enabled: @vulnerability_finding_signatures_enabled))
+ vulnerability_finding_signatures_enabled: @signatures_enabled))
end
def create_signatures(tracking)
@@ -208,7 +208,7 @@ module Gitlab
report.analyzer = ::Gitlab::Ci::Reports::Security::Analyzer.new(**params)
end
- def create_scanner(scanner_data = top_level_scanner)
+ def create_scanner(scanner_data)
return unless scanner_data.is_a?(Hash)
report.add_scanner(
diff --git a/lib/gitlab/ci/parsers/security/validators/schema_validator.rb b/lib/gitlab/ci/parsers/security/validators/schema_validator.rb
index c075ada725a..28d6620e5c4 100644
--- a/lib/gitlab/ci/parsers/security/validators/schema_validator.rb
+++ b/lib/gitlab/ci/parsers/security/validators/schema_validator.rb
@@ -7,14 +7,14 @@ module Gitlab
module Validators
class SchemaValidator
SUPPORTED_VERSIONS = {
- cluster_image_scanning: %w[14.0.4 14.0.5 14.0.6 14.1.0 14.1.1 14.1.2],
- container_scanning: %w[14.0.0 14.0.1 14.0.2 14.0.3 14.0.4 14.0.5 14.0.6 14.1.0 14.1.1 14.1.2],
- coverage_fuzzing: %w[14.0.0 14.0.1 14.0.2 14.0.3 14.0.4 14.0.5 14.0.6 14.1.0 14.1.1 14.1.2],
- dast: %w[14.0.0 14.0.1 14.0.2 14.0.3 14.0.4 14.0.5 14.0.6 14.1.0 14.1.1 14.1.2],
- api_fuzzing: %w[14.0.0 14.0.1 14.0.2 14.0.3 14.0.4 14.0.5 14.0.6 14.1.0 14.1.1 14.1.2],
- dependency_scanning: %w[14.0.0 14.0.1 14.0.2 14.0.3 14.0.4 14.0.5 14.0.6 14.1.0 14.1.1 14.1.2],
- sast: %w[14.0.0 14.0.1 14.0.2 14.0.3 14.0.4 14.0.5 14.0.6 14.1.0 14.1.1 14.1.2],
- secret_detection: %w[14.0.0 14.0.1 14.0.2 14.0.3 14.0.4 14.0.5 14.0.6 14.1.0 14.1.1 14.1.2]
+ cluster_image_scanning: %w[14.0.4 14.0.5 14.0.6 14.1.0 14.1.1 14.1.2 14.1.3 15.0.0],
+ container_scanning: %w[14.0.0 14.0.1 14.0.2 14.0.3 14.0.4 14.0.5 14.0.6 14.1.0 14.1.1 14.1.2 14.1.3 15.0.0],
+ coverage_fuzzing: %w[14.0.0 14.0.1 14.0.2 14.0.3 14.0.4 14.0.5 14.0.6 14.1.0 14.1.1 14.1.2 14.1.3 15.0.0],
+ dast: %w[14.0.0 14.0.1 14.0.2 14.0.3 14.0.4 14.0.5 14.0.6 14.1.0 14.1.1 14.1.2 14.1.3 15.0.0],
+ api_fuzzing: %w[14.0.0 14.0.1 14.0.2 14.0.3 14.0.4 14.0.5 14.0.6 14.1.0 14.1.1 14.1.2 14.1.3 15.0.0],
+ dependency_scanning: %w[14.0.0 14.0.1 14.0.2 14.0.3 14.0.4 14.0.5 14.0.6 14.1.0 14.1.1 14.1.2 14.1.3 15.0.0],
+ sast: %w[14.0.0 14.0.1 14.0.2 14.0.3 14.0.4 14.0.5 14.0.6 14.1.0 14.1.1 14.1.2 14.1.3 15.0.0],
+ secret_detection: %w[14.0.0 14.0.1 14.0.2 14.0.3 14.0.4 14.0.5 14.0.6 14.1.0 14.1.1 14.1.2 14.1.3 15.0.0]
}.freeze
VERSIONS_TO_REMOVE_IN_16_0 = [].freeze
diff --git a/lib/gitlab/ci/parsers/security/validators/schemas/14.1.3/cluster-image-scanning-report-format.json b/lib/gitlab/ci/parsers/security/validators/schemas/14.1.3/cluster-image-scanning-report-format.json
new file mode 100644
index 00000000000..db4c7ab1425
--- /dev/null
+++ b/lib/gitlab/ci/parsers/security/validators/schemas/14.1.3/cluster-image-scanning-report-format.json
@@ -0,0 +1,977 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "title": "Report format for GitLab Cluster Image Scanning",
+ "description": "This schema provides the the report format for Cluster Image Scanning (https://docs.gitlab.com/ee/user/application_security/cluster_image_scanning/).",
+ "definitions": {
+ "detail_type": {
+ "oneOf": [
+ {
+ "$ref": "#/definitions/named_list"
+ },
+ {
+ "$ref": "#/definitions/list"
+ },
+ {
+ "$ref": "#/definitions/table"
+ },
+ {
+ "$ref": "#/definitions/text"
+ },
+ {
+ "$ref": "#/definitions/url"
+ },
+ {
+ "$ref": "#/definitions/code"
+ },
+ {
+ "$ref": "#/definitions/value"
+ },
+ {
+ "$ref": "#/definitions/diff"
+ },
+ {
+ "$ref": "#/definitions/markdown"
+ },
+ {
+ "$ref": "#/definitions/commit"
+ },
+ {
+ "$ref": "#/definitions/file_location"
+ },
+ {
+ "$ref": "#/definitions/module_location"
+ }
+ ]
+ },
+ "text_value": {
+ "type": "string"
+ },
+ "named_field": {
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "$ref": "#/definitions/text_value",
+ "minLength": 1
+ },
+ "description": {
+ "$ref": "#/definitions/text_value"
+ }
+ }
+ },
+ "named_list": {
+ "type": "object",
+ "description": "An object with named and typed fields",
+ "required": [
+ "type",
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "named-list"
+ },
+ "items": {
+ "type": "object",
+ "patternProperties": {
+ "^.*$": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/named_field"
+ },
+ {
+ "$ref": "#/definitions/detail_type"
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "list": {
+ "type": "object",
+ "description": "A list of typed fields",
+ "required": [
+ "type",
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "list"
+ },
+ "items": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ }
+ }
+ },
+ "table": {
+ "type": "object",
+ "description": "A table of typed fields",
+ "required": [
+ "type",
+ "rows"
+ ],
+ "properties": {
+ "type": {
+ "const": "table"
+ },
+ "header": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ },
+ "rows": {
+ "type": "array",
+ "items": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ }
+ }
+ }
+ },
+ "text": {
+ "type": "object",
+ "description": "Raw text",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "text"
+ },
+ "value": {
+ "$ref": "#/definitions/text_value"
+ }
+ }
+ },
+ "url": {
+ "type": "object",
+ "description": "A single URL",
+ "required": [
+ "type",
+ "href"
+ ],
+ "properties": {
+ "type": {
+ "const": "url"
+ },
+ "text": {
+ "$ref": "#/definitions/text_value"
+ },
+ "href": {
+ "type": "string",
+ "minLength": 1,
+ "examples": [
+ "http://mysite.com"
+ ]
+ }
+ }
+ },
+ "code": {
+ "type": "object",
+ "description": "A codeblock",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "code"
+ },
+ "value": {
+ "type": "string"
+ },
+ "lang": {
+ "type": "string",
+ "description": "A programming language"
+ }
+ }
+ },
+ "value": {
+ "type": "object",
+ "description": "A field that can store a range of types of value",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "value"
+ },
+ "value": {
+ "type": [
+ "number",
+ "string",
+ "boolean"
+ ]
+ }
+ }
+ },
+ "diff": {
+ "type": "object",
+ "description": "A diff",
+ "required": [
+ "type",
+ "before",
+ "after"
+ ],
+ "properties": {
+ "type": {
+ "const": "diff"
+ },
+ "before": {
+ "type": "string"
+ },
+ "after": {
+ "type": "string"
+ }
+ }
+ },
+ "markdown": {
+ "type": "object",
+ "description": "GitLab flavoured markdown, see https://docs.gitlab.com/ee/user/markdown.html",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "markdown"
+ },
+ "value": {
+ "$ref": "#/definitions/text_value",
+ "examples": [
+ "Here is markdown `inline code` #1 [test](gitlab.com)\n\n![GitLab Logo](https://about.gitlab.com/images/press/logo/preview/gitlab-logo-white-preview.png)"
+ ]
+ }
+ }
+ },
+ "commit": {
+ "type": "object",
+ "description": "A commit/tag/branch within the GitLab project",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "commit"
+ },
+ "value": {
+ "type": "string",
+ "description": "The commit SHA",
+ "minLength": 1
+ }
+ }
+ },
+ "file_location": {
+ "type": "object",
+ "description": "A location within a file in the project",
+ "required": [
+ "type",
+ "file_name",
+ "line_start"
+ ],
+ "properties": {
+ "type": {
+ "const": "file-location"
+ },
+ "file_name": {
+ "type": "string",
+ "minLength": 1
+ },
+ "line_start": {
+ "type": "integer"
+ },
+ "line_end": {
+ "type": "integer"
+ }
+ }
+ },
+ "module_location": {
+ "type": "object",
+ "description": "A location within a binary module of the form module+relative_offset",
+ "required": [
+ "type",
+ "module_name",
+ "offset"
+ ],
+ "properties": {
+ "type": {
+ "const": "module-location"
+ },
+ "module_name": {
+ "type": "string",
+ "minLength": 1,
+ "examples": [
+ "compiled_binary"
+ ]
+ },
+ "offset": {
+ "type": "integer",
+ "examples": [
+ 100
+ ]
+ }
+ }
+ }
+ },
+ "self": {
+ "version": "14.1.3"
+ },
+ "required": [
+ "version",
+ "vulnerabilities"
+ ],
+ "additionalProperties": true,
+ "properties": {
+ "scan": {
+ "type": "object",
+ "required": [
+ "end_time",
+ "scanner",
+ "start_time",
+ "status",
+ "type"
+ ],
+ "properties": {
+ "end_time": {
+ "type": "string",
+ "description": "ISO8601 UTC value with format yyyy-mm-ddThh:mm:ss, representing when the scan finished.",
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}\\:\\d{2}\\:\\d{2}$",
+ "examples": [
+ "2020-01-28T03:26:02"
+ ]
+ },
+ "messages": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "description": "Communication intended for the initiator of a scan.",
+ "required": [
+ "level",
+ "value"
+ ],
+ "properties": {
+ "level": {
+ "type": "string",
+ "description": "Describes the severity of the communication. Use info to communicate normal scan behaviour; warn to communicate a potentially recoverable problem, or a partial error; fatal to communicate an issue that causes the scan to halt.",
+ "enum": [
+ "info",
+ "warn",
+ "fatal"
+ ],
+ "examples": [
+ "info"
+ ]
+ },
+ "value": {
+ "type": "string",
+ "description": "The message to communicate.",
+ "minLength": 1,
+ "examples": [
+ "Permission denied, scanning aborted"
+ ]
+ }
+ }
+ }
+ },
+ "analyzer": {
+ "type": "object",
+ "description": "Object defining the analyzer used to perform the scan. Analyzers typically delegate to an underlying scanner to run the scan.",
+ "required": [
+ "id",
+ "name",
+ "version",
+ "vendor"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Unique id that identifies the analyzer.",
+ "minLength": 1,
+ "examples": [
+ "gitlab-dast"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "description": "A human readable value that identifies the analyzer, not required to be unique.",
+ "minLength": 1,
+ "examples": [
+ "GitLab DAST"
+ ]
+ },
+ "url": {
+ "type": "string",
+ "format": "uri",
+ "pattern": "^https?://.+",
+ "description": "A link to more information about the analyzer.",
+ "examples": [
+ "https://docs.gitlab.com/ee/user/application_security/dast"
+ ]
+ },
+ "vendor": {
+ "description": "The vendor/maintainer of the analyzer.",
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The name of the vendor.",
+ "minLength": 1,
+ "examples": [
+ "GitLab"
+ ]
+ }
+ }
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the analyzer.",
+ "minLength": 1,
+ "examples": [
+ "1.0.2"
+ ]
+ }
+ }
+ },
+ "scanner": {
+ "type": "object",
+ "description": "Object defining the scanner used to perform the scan.",
+ "required": [
+ "id",
+ "name",
+ "version",
+ "vendor"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Unique id that identifies the scanner.",
+ "minLength": 1,
+ "examples": [
+ "my-sast-scanner"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "description": "A human readable value that identifies the scanner, not required to be unique.",
+ "minLength": 1,
+ "examples": [
+ "My SAST Scanner"
+ ]
+ },
+ "url": {
+ "type": "string",
+ "description": "A link to more information about the scanner.",
+ "examples": [
+ "https://scanner.url"
+ ]
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the scanner.",
+ "minLength": 1,
+ "examples": [
+ "1.0.2"
+ ]
+ },
+ "vendor": {
+ "description": "The vendor/maintainer of the scanner.",
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The name of the vendor.",
+ "minLength": 1,
+ "examples": [
+ "GitLab"
+ ]
+ }
+ }
+ }
+ }
+ },
+ "start_time": {
+ "type": "string",
+ "description": "ISO8601 UTC value with format yyyy-mm-ddThh:mm:ss, representing when the scan started.",
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}\\:\\d{2}\\:\\d{2}$",
+ "examples": [
+ "2020-02-14T16:01:59"
+ ]
+ },
+ "status": {
+ "type": "string",
+ "description": "Result of the scan.",
+ "enum": [
+ "success",
+ "failure"
+ ]
+ },
+ "type": {
+ "type": "string",
+ "description": "Type of the scan.",
+ "enum": [
+ "cluster_image_scanning"
+ ]
+ }
+ }
+ },
+ "schema": {
+ "type": "string",
+ "description": "URI pointing to the validating security report schema.",
+ "format": "uri"
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the schema to which the JSON report conforms.",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ },
+ "vulnerabilities": {
+ "type": "array",
+ "description": "Array of vulnerability objects.",
+ "items": {
+ "type": "object",
+ "description": "Describes the vulnerability using GitLab Flavored Markdown",
+ "required": [
+ "category",
+ "cve",
+ "identifiers",
+ "location",
+ "scanner"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Unique identifier of the vulnerability. This is recommended to be a UUID.",
+ "examples": [
+ "642735a5-1425-428d-8d4e-3c854885a3c9"
+ ]
+ },
+ "category": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Describes where this vulnerability belongs (for example, SAST, Dependency Scanning, and so on)."
+ },
+ "name": {
+ "type": "string",
+ "description": "The name of the vulnerability. This must not include the finding's specific information."
+ },
+ "message": {
+ "type": "string",
+ "description": "A short text section that describes the vulnerability. This may include the finding's specific information."
+ },
+ "description": {
+ "type": "string",
+ "description": "A long text section describing the vulnerability more fully."
+ },
+ "cve": {
+ "type": "string",
+ "description": "(Deprecated - use vulnerabilities[].id instead) A fingerprint string value that represents a concrete finding. This is used to determine whether two findings are same, which may not be 100% accurate. Note that this is NOT a CVE as described by https://cve.mitre.org/."
+ },
+ "severity": {
+ "type": "string",
+ "description": "How much the vulnerability impacts the software. Possible values are Info, Unknown, Low, Medium, High, or Critical. Note that some analyzers may not report all these possible values.",
+ "enum": [
+ "Info",
+ "Unknown",
+ "Low",
+ "Medium",
+ "High",
+ "Critical"
+ ]
+ },
+ "confidence": {
+ "type": "string",
+ "description": "How reliable the vulnerability's assessment is. Possible values are Ignore, Unknown, Experimental, Low, Medium, High, and Confirmed. Note that some analyzers may not report all these possible values.",
+ "enum": [
+ "Ignore",
+ "Unknown",
+ "Experimental",
+ "Low",
+ "Medium",
+ "High",
+ "Confirmed"
+ ]
+ },
+ "solution": {
+ "type": "string",
+ "description": "Explanation of how to fix the vulnerability."
+ },
+ "scanner": {
+ "description": "Describes the scanner used to find this vulnerability.",
+ "type": "object",
+ "required": [
+ "id",
+ "name"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "minLength": 1,
+ "description": "The scanner's ID, as a snake_case string."
+ },
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Human-readable name of the scanner."
+ }
+ }
+ },
+ "identifiers": {
+ "type": "array",
+ "minItems": 1,
+ "description": "An ordered array of references that identify a vulnerability on internal or external databases. The first identifier is the Primary Identifier, which has special meaning.",
+ "items": {
+ "type": "object",
+ "required": [
+ "type",
+ "name",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "for example, cve, cwe, osvdb, usn, or an analyzer-dependent type such as gemnasium).",
+ "minLength": 1
+ },
+ "name": {
+ "type": "string",
+ "description": "Human-readable name of the identifier.",
+ "minLength": 1
+ },
+ "url": {
+ "type": "string",
+ "description": "URL of the identifier's documentation.",
+ "format": "uri"
+ },
+ "value": {
+ "type": "string",
+ "description": "Value of the identifier, for matching purpose.",
+ "minLength": 1
+ }
+ }
+ }
+ },
+ "links": {
+ "type": "array",
+ "description": "An array of references to external documentation or articles that describe the vulnerability.",
+ "items": {
+ "type": "object",
+ "required": [
+ "url"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Name of the vulnerability details link."
+ },
+ "url": {
+ "type": "string",
+ "description": "URL of the vulnerability details document.",
+ "format": "uri"
+ }
+ }
+ }
+ },
+ "details": {
+ "$ref": "#/definitions/named_list/properties/items"
+ },
+ "tracking": {
+ "description": "Describes how this vulnerability should be tracked as the project changes.",
+ "oneOf": [
+ {
+ "description": "Declares that a series of items should be tracked using source-specific tracking methods.",
+ "required": [
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "source"
+ },
+ "items": {
+ "type": "array",
+ "items": {
+ "description": "An item that should be tracked using source-specific tracking methods.",
+ "type": "object",
+ "required": [
+ "signatures"
+ ],
+ "properties": {
+ "file": {
+ "type": "string",
+ "description": "Path to the file where the vulnerability is located."
+ },
+ "start_line": {
+ "type": "number",
+ "description": "The first line of the file that includes the vulnerability."
+ },
+ "end_line": {
+ "type": "number",
+ "description": "The last line of the file that includes the vulnerability."
+ },
+ "signatures": {
+ "type": "array",
+ "description": "An array of calculated tracking signatures for this tracking item.",
+ "minItems": 1,
+ "items": {
+ "description": "A calculated tracking signature value and metadata.",
+ "required": [
+ "algorithm",
+ "value"
+ ],
+ "properties": {
+ "algorithm": {
+ "type": "string",
+ "description": "The algorithm used to generate the signature."
+ },
+ "value": {
+ "type": "string",
+ "description": "The result of this signature algorithm."
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "Each tracking type must declare its own type."
+ }
+ }
+ },
+ "flags": {
+ "description": "Flags that can be attached to vulnerabilities.",
+ "type": "array",
+ "items": {
+ "type": "object",
+ "description": "Informational flags identified and assigned to a vulnerability.",
+ "required": [
+ "type",
+ "origin",
+ "description"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Result of the scan.",
+ "enum": [
+ "flagged-as-likely-false-positive"
+ ]
+ },
+ "origin": {
+ "minLength": 1,
+ "description": "Tool that issued the flag.",
+ "type": "string"
+ },
+ "description": {
+ "minLength": 1,
+ "description": "What the flag is about.",
+ "type": "string"
+ }
+ }
+ }
+ },
+ "location": {
+ "type": "object",
+ "description": "Identifies the vulnerability's location.",
+ "required": [
+ "dependency",
+ "image",
+ "kubernetes_resource"
+ ],
+ "properties": {
+ "dependency": {
+ "type": "object",
+ "description": "Describes the dependency of a project where the vulnerability is located.",
+ "properties": {
+ "package": {
+ "type": "object",
+ "description": "Provides information on the package where the vulnerability is located.",
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Name of the package where the vulnerability is located."
+ }
+ }
+ },
+ "version": {
+ "type": "string",
+ "description": "Version of the vulnerable package."
+ },
+ "iid": {
+ "description": "ID that identifies the dependency in the scope of a dependency file.",
+ "type": "number"
+ },
+ "direct": {
+ "type": "boolean",
+ "description": "Tells whether this is a direct, top-level dependency of the scanned project."
+ },
+ "dependency_path": {
+ "type": "array",
+ "description": "Ancestors of the dependency, starting from a direct project dependency, and ending with an immediate parent of the dependency. The dependency itself is excluded from the path. Direct dependencies have no path.",
+ "items": {
+ "type": "object",
+ "required": [
+ "iid"
+ ],
+ "properties": {
+ "iid": {
+ "type": "number",
+ "description": "ID that is unique in the scope of a parent object, and specific to the resource type."
+ }
+ }
+ }
+ }
+ }
+ },
+ "operating_system": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 255,
+ "description": "The operating system that contains the vulnerable package."
+ },
+ "image": {
+ "type": "string",
+ "minLength": 1,
+ "description": "The analyzed Docker image.",
+ "examples": [
+ "index.docker.io/library/nginx:1.21"
+ ]
+ },
+ "kubernetes_resource": {
+ "type": "object",
+ "description": "The specific Kubernetes resource that was scanned.",
+ "required": [
+ "namespace",
+ "kind",
+ "name",
+ "container_name"
+ ],
+ "properties": {
+ "namespace": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 255,
+ "description": "The Kubernetes namespace the resource that had its image scanned.",
+ "examples": [
+ "default",
+ "staging",
+ "production"
+ ]
+ },
+ "kind": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 255,
+ "description": "The Kubernetes kind the resource that had its image scanned.",
+ "examples": [
+ "Deployment",
+ "DaemonSet"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 255,
+ "description": "The name of the resource that had its image scanned.",
+ "examples": [
+ "nginx-ingress"
+ ]
+ },
+ "container_name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 255,
+ "description": "The name of the container that had its image scanned.",
+ "examples": [
+ "nginx"
+ ]
+ },
+ "agent_id": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 255,
+ "description": "The GitLab ID of the Kubernetes Agent which performed the scan.",
+ "examples": [
+ "1234"
+ ]
+ },
+ "cluster_id": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 255,
+ "description": "The GitLab ID of the Kubernetes cluster when using cluster integration.",
+ "examples": [
+ "1234"
+ ]
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "remediations": {
+ "type": "array",
+ "description": "An array of objects containing information on available remediations, along with patch diffs to apply.",
+ "items": {
+ "type": "object",
+ "required": [
+ "fixes",
+ "summary",
+ "diff"
+ ],
+ "properties": {
+ "fixes": {
+ "type": "array",
+ "description": "An array of strings that represent references to vulnerabilities fixed by this remediation.",
+ "items": {
+ "type": "object",
+ "required": [
+ "cve"
+ ],
+ "properties": {
+ "cve": {
+ "type": "string",
+ "description": "(Deprecated - use vulnerabilities[].id instead) A fingerprint string value that represents a concrete finding. This is used to determine whether two findings are same, which may not be 100% accurate. Note that this is NOT a CVE as described by https://cve.mitre.org/."
+ }
+ }
+ }
+ },
+ "summary": {
+ "type": "string",
+ "minLength": 1,
+ "description": "An overview of how the vulnerabilities were fixed."
+ },
+ "diff": {
+ "type": "string",
+ "minLength": 1,
+ "description": "A base64-encoded remediation code diff, compatible with git apply."
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/lib/gitlab/ci/parsers/security/validators/schemas/14.1.3/container-scanning-report-format.json b/lib/gitlab/ci/parsers/security/validators/schemas/14.1.3/container-scanning-report-format.json
new file mode 100644
index 00000000000..641cfc82e48
--- /dev/null
+++ b/lib/gitlab/ci/parsers/security/validators/schemas/14.1.3/container-scanning-report-format.json
@@ -0,0 +1,911 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "title": "Report format for GitLab Container Scanning",
+ "description": "This schema provides the the report format for Container Scanning (https://docs.gitlab.com/ee/user/application_security/container_scanning).",
+ "definitions": {
+ "detail_type": {
+ "oneOf": [
+ {
+ "$ref": "#/definitions/named_list"
+ },
+ {
+ "$ref": "#/definitions/list"
+ },
+ {
+ "$ref": "#/definitions/table"
+ },
+ {
+ "$ref": "#/definitions/text"
+ },
+ {
+ "$ref": "#/definitions/url"
+ },
+ {
+ "$ref": "#/definitions/code"
+ },
+ {
+ "$ref": "#/definitions/value"
+ },
+ {
+ "$ref": "#/definitions/diff"
+ },
+ {
+ "$ref": "#/definitions/markdown"
+ },
+ {
+ "$ref": "#/definitions/commit"
+ },
+ {
+ "$ref": "#/definitions/file_location"
+ },
+ {
+ "$ref": "#/definitions/module_location"
+ }
+ ]
+ },
+ "text_value": {
+ "type": "string"
+ },
+ "named_field": {
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "$ref": "#/definitions/text_value",
+ "minLength": 1
+ },
+ "description": {
+ "$ref": "#/definitions/text_value"
+ }
+ }
+ },
+ "named_list": {
+ "type": "object",
+ "description": "An object with named and typed fields",
+ "required": [
+ "type",
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "named-list"
+ },
+ "items": {
+ "type": "object",
+ "patternProperties": {
+ "^.*$": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/named_field"
+ },
+ {
+ "$ref": "#/definitions/detail_type"
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "list": {
+ "type": "object",
+ "description": "A list of typed fields",
+ "required": [
+ "type",
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "list"
+ },
+ "items": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ }
+ }
+ },
+ "table": {
+ "type": "object",
+ "description": "A table of typed fields",
+ "required": [
+ "type",
+ "rows"
+ ],
+ "properties": {
+ "type": {
+ "const": "table"
+ },
+ "header": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ },
+ "rows": {
+ "type": "array",
+ "items": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ }
+ }
+ }
+ },
+ "text": {
+ "type": "object",
+ "description": "Raw text",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "text"
+ },
+ "value": {
+ "$ref": "#/definitions/text_value"
+ }
+ }
+ },
+ "url": {
+ "type": "object",
+ "description": "A single URL",
+ "required": [
+ "type",
+ "href"
+ ],
+ "properties": {
+ "type": {
+ "const": "url"
+ },
+ "text": {
+ "$ref": "#/definitions/text_value"
+ },
+ "href": {
+ "type": "string",
+ "minLength": 1,
+ "examples": [
+ "http://mysite.com"
+ ]
+ }
+ }
+ },
+ "code": {
+ "type": "object",
+ "description": "A codeblock",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "code"
+ },
+ "value": {
+ "type": "string"
+ },
+ "lang": {
+ "type": "string",
+ "description": "A programming language"
+ }
+ }
+ },
+ "value": {
+ "type": "object",
+ "description": "A field that can store a range of types of value",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "value"
+ },
+ "value": {
+ "type": [
+ "number",
+ "string",
+ "boolean"
+ ]
+ }
+ }
+ },
+ "diff": {
+ "type": "object",
+ "description": "A diff",
+ "required": [
+ "type",
+ "before",
+ "after"
+ ],
+ "properties": {
+ "type": {
+ "const": "diff"
+ },
+ "before": {
+ "type": "string"
+ },
+ "after": {
+ "type": "string"
+ }
+ }
+ },
+ "markdown": {
+ "type": "object",
+ "description": "GitLab flavoured markdown, see https://docs.gitlab.com/ee/user/markdown.html",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "markdown"
+ },
+ "value": {
+ "$ref": "#/definitions/text_value",
+ "examples": [
+ "Here is markdown `inline code` #1 [test](gitlab.com)\n\n![GitLab Logo](https://about.gitlab.com/images/press/logo/preview/gitlab-logo-white-preview.png)"
+ ]
+ }
+ }
+ },
+ "commit": {
+ "type": "object",
+ "description": "A commit/tag/branch within the GitLab project",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "commit"
+ },
+ "value": {
+ "type": "string",
+ "description": "The commit SHA",
+ "minLength": 1
+ }
+ }
+ },
+ "file_location": {
+ "type": "object",
+ "description": "A location within a file in the project",
+ "required": [
+ "type",
+ "file_name",
+ "line_start"
+ ],
+ "properties": {
+ "type": {
+ "const": "file-location"
+ },
+ "file_name": {
+ "type": "string",
+ "minLength": 1
+ },
+ "line_start": {
+ "type": "integer"
+ },
+ "line_end": {
+ "type": "integer"
+ }
+ }
+ },
+ "module_location": {
+ "type": "object",
+ "description": "A location within a binary module of the form module+relative_offset",
+ "required": [
+ "type",
+ "module_name",
+ "offset"
+ ],
+ "properties": {
+ "type": {
+ "const": "module-location"
+ },
+ "module_name": {
+ "type": "string",
+ "minLength": 1,
+ "examples": [
+ "compiled_binary"
+ ]
+ },
+ "offset": {
+ "type": "integer",
+ "examples": [
+ 100
+ ]
+ }
+ }
+ }
+ },
+ "self": {
+ "version": "14.1.3"
+ },
+ "required": [
+ "version",
+ "vulnerabilities"
+ ],
+ "additionalProperties": true,
+ "properties": {
+ "scan": {
+ "type": "object",
+ "required": [
+ "end_time",
+ "scanner",
+ "start_time",
+ "status",
+ "type"
+ ],
+ "properties": {
+ "end_time": {
+ "type": "string",
+ "description": "ISO8601 UTC value with format yyyy-mm-ddThh:mm:ss, representing when the scan finished.",
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}\\:\\d{2}\\:\\d{2}$",
+ "examples": [
+ "2020-01-28T03:26:02"
+ ]
+ },
+ "messages": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "description": "Communication intended for the initiator of a scan.",
+ "required": [
+ "level",
+ "value"
+ ],
+ "properties": {
+ "level": {
+ "type": "string",
+ "description": "Describes the severity of the communication. Use info to communicate normal scan behaviour; warn to communicate a potentially recoverable problem, or a partial error; fatal to communicate an issue that causes the scan to halt.",
+ "enum": [
+ "info",
+ "warn",
+ "fatal"
+ ],
+ "examples": [
+ "info"
+ ]
+ },
+ "value": {
+ "type": "string",
+ "description": "The message to communicate.",
+ "minLength": 1,
+ "examples": [
+ "Permission denied, scanning aborted"
+ ]
+ }
+ }
+ }
+ },
+ "analyzer": {
+ "type": "object",
+ "description": "Object defining the analyzer used to perform the scan. Analyzers typically delegate to an underlying scanner to run the scan.",
+ "required": [
+ "id",
+ "name",
+ "version",
+ "vendor"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Unique id that identifies the analyzer.",
+ "minLength": 1,
+ "examples": [
+ "gitlab-dast"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "description": "A human readable value that identifies the analyzer, not required to be unique.",
+ "minLength": 1,
+ "examples": [
+ "GitLab DAST"
+ ]
+ },
+ "url": {
+ "type": "string",
+ "format": "uri",
+ "pattern": "^https?://.+",
+ "description": "A link to more information about the analyzer.",
+ "examples": [
+ "https://docs.gitlab.com/ee/user/application_security/dast"
+ ]
+ },
+ "vendor": {
+ "description": "The vendor/maintainer of the analyzer.",
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The name of the vendor.",
+ "minLength": 1,
+ "examples": [
+ "GitLab"
+ ]
+ }
+ }
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the analyzer.",
+ "minLength": 1,
+ "examples": [
+ "1.0.2"
+ ]
+ }
+ }
+ },
+ "scanner": {
+ "type": "object",
+ "description": "Object defining the scanner used to perform the scan.",
+ "required": [
+ "id",
+ "name",
+ "version",
+ "vendor"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Unique id that identifies the scanner.",
+ "minLength": 1,
+ "examples": [
+ "my-sast-scanner"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "description": "A human readable value that identifies the scanner, not required to be unique.",
+ "minLength": 1,
+ "examples": [
+ "My SAST Scanner"
+ ]
+ },
+ "url": {
+ "type": "string",
+ "description": "A link to more information about the scanner.",
+ "examples": [
+ "https://scanner.url"
+ ]
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the scanner.",
+ "minLength": 1,
+ "examples": [
+ "1.0.2"
+ ]
+ },
+ "vendor": {
+ "description": "The vendor/maintainer of the scanner.",
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The name of the vendor.",
+ "minLength": 1,
+ "examples": [
+ "GitLab"
+ ]
+ }
+ }
+ }
+ }
+ },
+ "start_time": {
+ "type": "string",
+ "description": "ISO8601 UTC value with format yyyy-mm-ddThh:mm:ss, representing when the scan started.",
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}\\:\\d{2}\\:\\d{2}$",
+ "examples": [
+ "2020-02-14T16:01:59"
+ ]
+ },
+ "status": {
+ "type": "string",
+ "description": "Result of the scan.",
+ "enum": [
+ "success",
+ "failure"
+ ]
+ },
+ "type": {
+ "type": "string",
+ "description": "Type of the scan.",
+ "enum": [
+ "container_scanning"
+ ]
+ }
+ }
+ },
+ "schema": {
+ "type": "string",
+ "description": "URI pointing to the validating security report schema.",
+ "format": "uri"
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the schema to which the JSON report conforms.",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ },
+ "vulnerabilities": {
+ "type": "array",
+ "description": "Array of vulnerability objects.",
+ "items": {
+ "type": "object",
+ "description": "Describes the vulnerability using GitLab Flavored Markdown",
+ "required": [
+ "category",
+ "cve",
+ "identifiers",
+ "location",
+ "scanner"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Unique identifier of the vulnerability. This is recommended to be a UUID.",
+ "examples": [
+ "642735a5-1425-428d-8d4e-3c854885a3c9"
+ ]
+ },
+ "category": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Describes where this vulnerability belongs (for example, SAST, Dependency Scanning, and so on)."
+ },
+ "name": {
+ "type": "string",
+ "description": "The name of the vulnerability. This must not include the finding's specific information."
+ },
+ "message": {
+ "type": "string",
+ "description": "A short text section that describes the vulnerability. This may include the finding's specific information."
+ },
+ "description": {
+ "type": "string",
+ "description": "A long text section describing the vulnerability more fully."
+ },
+ "cve": {
+ "type": "string",
+ "description": "(Deprecated - use vulnerabilities[].id instead) A fingerprint string value that represents a concrete finding. This is used to determine whether two findings are same, which may not be 100% accurate. Note that this is NOT a CVE as described by https://cve.mitre.org/."
+ },
+ "severity": {
+ "type": "string",
+ "description": "How much the vulnerability impacts the software. Possible values are Info, Unknown, Low, Medium, High, or Critical. Note that some analyzers may not report all these possible values.",
+ "enum": [
+ "Info",
+ "Unknown",
+ "Low",
+ "Medium",
+ "High",
+ "Critical"
+ ]
+ },
+ "confidence": {
+ "type": "string",
+ "description": "How reliable the vulnerability's assessment is. Possible values are Ignore, Unknown, Experimental, Low, Medium, High, and Confirmed. Note that some analyzers may not report all these possible values.",
+ "enum": [
+ "Ignore",
+ "Unknown",
+ "Experimental",
+ "Low",
+ "Medium",
+ "High",
+ "Confirmed"
+ ]
+ },
+ "solution": {
+ "type": "string",
+ "description": "Explanation of how to fix the vulnerability."
+ },
+ "scanner": {
+ "description": "Describes the scanner used to find this vulnerability.",
+ "type": "object",
+ "required": [
+ "id",
+ "name"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "minLength": 1,
+ "description": "The scanner's ID, as a snake_case string."
+ },
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Human-readable name of the scanner."
+ }
+ }
+ },
+ "identifiers": {
+ "type": "array",
+ "minItems": 1,
+ "description": "An ordered array of references that identify a vulnerability on internal or external databases. The first identifier is the Primary Identifier, which has special meaning.",
+ "items": {
+ "type": "object",
+ "required": [
+ "type",
+ "name",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "for example, cve, cwe, osvdb, usn, or an analyzer-dependent type such as gemnasium).",
+ "minLength": 1
+ },
+ "name": {
+ "type": "string",
+ "description": "Human-readable name of the identifier.",
+ "minLength": 1
+ },
+ "url": {
+ "type": "string",
+ "description": "URL of the identifier's documentation.",
+ "format": "uri"
+ },
+ "value": {
+ "type": "string",
+ "description": "Value of the identifier, for matching purpose.",
+ "minLength": 1
+ }
+ }
+ }
+ },
+ "links": {
+ "type": "array",
+ "description": "An array of references to external documentation or articles that describe the vulnerability.",
+ "items": {
+ "type": "object",
+ "required": [
+ "url"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Name of the vulnerability details link."
+ },
+ "url": {
+ "type": "string",
+ "description": "URL of the vulnerability details document.",
+ "format": "uri"
+ }
+ }
+ }
+ },
+ "details": {
+ "$ref": "#/definitions/named_list/properties/items"
+ },
+ "tracking": {
+ "description": "Describes how this vulnerability should be tracked as the project changes.",
+ "oneOf": [
+ {
+ "description": "Declares that a series of items should be tracked using source-specific tracking methods.",
+ "required": [
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "source"
+ },
+ "items": {
+ "type": "array",
+ "items": {
+ "description": "An item that should be tracked using source-specific tracking methods.",
+ "type": "object",
+ "required": [
+ "signatures"
+ ],
+ "properties": {
+ "file": {
+ "type": "string",
+ "description": "Path to the file where the vulnerability is located."
+ },
+ "start_line": {
+ "type": "number",
+ "description": "The first line of the file that includes the vulnerability."
+ },
+ "end_line": {
+ "type": "number",
+ "description": "The last line of the file that includes the vulnerability."
+ },
+ "signatures": {
+ "type": "array",
+ "description": "An array of calculated tracking signatures for this tracking item.",
+ "minItems": 1,
+ "items": {
+ "description": "A calculated tracking signature value and metadata.",
+ "required": [
+ "algorithm",
+ "value"
+ ],
+ "properties": {
+ "algorithm": {
+ "type": "string",
+ "description": "The algorithm used to generate the signature."
+ },
+ "value": {
+ "type": "string",
+ "description": "The result of this signature algorithm."
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "Each tracking type must declare its own type."
+ }
+ }
+ },
+ "flags": {
+ "description": "Flags that can be attached to vulnerabilities.",
+ "type": "array",
+ "items": {
+ "type": "object",
+ "description": "Informational flags identified and assigned to a vulnerability.",
+ "required": [
+ "type",
+ "origin",
+ "description"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Result of the scan.",
+ "enum": [
+ "flagged-as-likely-false-positive"
+ ]
+ },
+ "origin": {
+ "minLength": 1,
+ "description": "Tool that issued the flag.",
+ "type": "string"
+ },
+ "description": {
+ "minLength": 1,
+ "description": "What the flag is about.",
+ "type": "string"
+ }
+ }
+ }
+ },
+ "location": {
+ "type": "object",
+ "description": "Identifies the vulnerability's location.",
+ "required": [
+ "dependency",
+ "operating_system",
+ "image"
+ ],
+ "properties": {
+ "dependency": {
+ "type": "object",
+ "description": "Describes the dependency of a project where the vulnerability is located.",
+ "properties": {
+ "package": {
+ "type": "object",
+ "description": "Provides information on the package where the vulnerability is located.",
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Name of the package where the vulnerability is located."
+ }
+ }
+ },
+ "version": {
+ "type": "string",
+ "description": "Version of the vulnerable package."
+ },
+ "iid": {
+ "description": "ID that identifies the dependency in the scope of a dependency file.",
+ "type": "number"
+ },
+ "direct": {
+ "type": "boolean",
+ "description": "Tells whether this is a direct, top-level dependency of the scanned project."
+ },
+ "dependency_path": {
+ "type": "array",
+ "description": "Ancestors of the dependency, starting from a direct project dependency, and ending with an immediate parent of the dependency. The dependency itself is excluded from the path. Direct dependencies have no path.",
+ "items": {
+ "type": "object",
+ "required": [
+ "iid"
+ ],
+ "properties": {
+ "iid": {
+ "type": "number",
+ "description": "ID that is unique in the scope of a parent object, and specific to the resource type."
+ }
+ }
+ }
+ }
+ }
+ },
+ "operating_system": {
+ "type": "string",
+ "minLength": 1,
+ "description": "The operating system that contains the vulnerable package."
+ },
+ "image": {
+ "type": "string",
+ "minLength": 1,
+ "pattern": "^[^:]+(:\\d+[^:]*)?:[^:]+(:[^:]+)?$",
+ "description": "The analyzed Docker image."
+ },
+ "default_branch_image": {
+ "type": "string",
+ "maxLength": 255,
+ "pattern": "^[a-zA-Z0-9/_.-]+(:\\d+[a-zA-Z0-9/_.-]*)?:[a-zA-Z0-9_.-]+$",
+ "description": "The name of the image on the default branch."
+ }
+ }
+ }
+ }
+ }
+ },
+ "remediations": {
+ "type": "array",
+ "description": "An array of objects containing information on available remediations, along with patch diffs to apply.",
+ "items": {
+ "type": "object",
+ "required": [
+ "fixes",
+ "summary",
+ "diff"
+ ],
+ "properties": {
+ "fixes": {
+ "type": "array",
+ "description": "An array of strings that represent references to vulnerabilities fixed by this remediation.",
+ "items": {
+ "type": "object",
+ "required": [
+ "cve"
+ ],
+ "properties": {
+ "cve": {
+ "type": "string",
+ "description": "(Deprecated - use vulnerabilities[].id instead) A fingerprint string value that represents a concrete finding. This is used to determine whether two findings are same, which may not be 100% accurate. Note that this is NOT a CVE as described by https://cve.mitre.org/."
+ }
+ }
+ }
+ },
+ "summary": {
+ "type": "string",
+ "minLength": 1,
+ "description": "An overview of how the vulnerabilities were fixed."
+ },
+ "diff": {
+ "type": "string",
+ "minLength": 1,
+ "description": "A base64-encoded remediation code diff, compatible with git apply."
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/lib/gitlab/ci/parsers/security/validators/schemas/14.1.3/coverage-fuzzing-report-format.json b/lib/gitlab/ci/parsers/security/validators/schemas/14.1.3/coverage-fuzzing-report-format.json
new file mode 100644
index 00000000000..59aa172444d
--- /dev/null
+++ b/lib/gitlab/ci/parsers/security/validators/schemas/14.1.3/coverage-fuzzing-report-format.json
@@ -0,0 +1,874 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "title": "Report format for GitLab Fuzz Testing",
+ "description": "This schema provides the report format for Coverage Guided Fuzz Testing (https://docs.gitlab.com/ee/user/application_security/coverage_fuzzing).",
+ "definitions": {
+ "detail_type": {
+ "oneOf": [
+ {
+ "$ref": "#/definitions/named_list"
+ },
+ {
+ "$ref": "#/definitions/list"
+ },
+ {
+ "$ref": "#/definitions/table"
+ },
+ {
+ "$ref": "#/definitions/text"
+ },
+ {
+ "$ref": "#/definitions/url"
+ },
+ {
+ "$ref": "#/definitions/code"
+ },
+ {
+ "$ref": "#/definitions/value"
+ },
+ {
+ "$ref": "#/definitions/diff"
+ },
+ {
+ "$ref": "#/definitions/markdown"
+ },
+ {
+ "$ref": "#/definitions/commit"
+ },
+ {
+ "$ref": "#/definitions/file_location"
+ },
+ {
+ "$ref": "#/definitions/module_location"
+ }
+ ]
+ },
+ "text_value": {
+ "type": "string"
+ },
+ "named_field": {
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "$ref": "#/definitions/text_value",
+ "minLength": 1
+ },
+ "description": {
+ "$ref": "#/definitions/text_value"
+ }
+ }
+ },
+ "named_list": {
+ "type": "object",
+ "description": "An object with named and typed fields",
+ "required": [
+ "type",
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "named-list"
+ },
+ "items": {
+ "type": "object",
+ "patternProperties": {
+ "^.*$": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/named_field"
+ },
+ {
+ "$ref": "#/definitions/detail_type"
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "list": {
+ "type": "object",
+ "description": "A list of typed fields",
+ "required": [
+ "type",
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "list"
+ },
+ "items": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ }
+ }
+ },
+ "table": {
+ "type": "object",
+ "description": "A table of typed fields",
+ "required": [
+ "type",
+ "rows"
+ ],
+ "properties": {
+ "type": {
+ "const": "table"
+ },
+ "header": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ },
+ "rows": {
+ "type": "array",
+ "items": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ }
+ }
+ }
+ },
+ "text": {
+ "type": "object",
+ "description": "Raw text",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "text"
+ },
+ "value": {
+ "$ref": "#/definitions/text_value"
+ }
+ }
+ },
+ "url": {
+ "type": "object",
+ "description": "A single URL",
+ "required": [
+ "type",
+ "href"
+ ],
+ "properties": {
+ "type": {
+ "const": "url"
+ },
+ "text": {
+ "$ref": "#/definitions/text_value"
+ },
+ "href": {
+ "type": "string",
+ "minLength": 1,
+ "examples": [
+ "http://mysite.com"
+ ]
+ }
+ }
+ },
+ "code": {
+ "type": "object",
+ "description": "A codeblock",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "code"
+ },
+ "value": {
+ "type": "string"
+ },
+ "lang": {
+ "type": "string",
+ "description": "A programming language"
+ }
+ }
+ },
+ "value": {
+ "type": "object",
+ "description": "A field that can store a range of types of value",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "value"
+ },
+ "value": {
+ "type": [
+ "number",
+ "string",
+ "boolean"
+ ]
+ }
+ }
+ },
+ "diff": {
+ "type": "object",
+ "description": "A diff",
+ "required": [
+ "type",
+ "before",
+ "after"
+ ],
+ "properties": {
+ "type": {
+ "const": "diff"
+ },
+ "before": {
+ "type": "string"
+ },
+ "after": {
+ "type": "string"
+ }
+ }
+ },
+ "markdown": {
+ "type": "object",
+ "description": "GitLab flavoured markdown, see https://docs.gitlab.com/ee/user/markdown.html",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "markdown"
+ },
+ "value": {
+ "$ref": "#/definitions/text_value",
+ "examples": [
+ "Here is markdown `inline code` #1 [test](gitlab.com)\n\n![GitLab Logo](https://about.gitlab.com/images/press/logo/preview/gitlab-logo-white-preview.png)"
+ ]
+ }
+ }
+ },
+ "commit": {
+ "type": "object",
+ "description": "A commit/tag/branch within the GitLab project",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "commit"
+ },
+ "value": {
+ "type": "string",
+ "description": "The commit SHA",
+ "minLength": 1
+ }
+ }
+ },
+ "file_location": {
+ "type": "object",
+ "description": "A location within a file in the project",
+ "required": [
+ "type",
+ "file_name",
+ "line_start"
+ ],
+ "properties": {
+ "type": {
+ "const": "file-location"
+ },
+ "file_name": {
+ "type": "string",
+ "minLength": 1
+ },
+ "line_start": {
+ "type": "integer"
+ },
+ "line_end": {
+ "type": "integer"
+ }
+ }
+ },
+ "module_location": {
+ "type": "object",
+ "description": "A location within a binary module of the form module+relative_offset",
+ "required": [
+ "type",
+ "module_name",
+ "offset"
+ ],
+ "properties": {
+ "type": {
+ "const": "module-location"
+ },
+ "module_name": {
+ "type": "string",
+ "minLength": 1,
+ "examples": [
+ "compiled_binary"
+ ]
+ },
+ "offset": {
+ "type": "integer",
+ "examples": [
+ 100
+ ]
+ }
+ }
+ }
+ },
+ "self": {
+ "version": "14.1.3"
+ },
+ "required": [
+ "version",
+ "vulnerabilities"
+ ],
+ "additionalProperties": true,
+ "properties": {
+ "scan": {
+ "type": "object",
+ "required": [
+ "end_time",
+ "scanner",
+ "start_time",
+ "status",
+ "type"
+ ],
+ "properties": {
+ "end_time": {
+ "type": "string",
+ "description": "ISO8601 UTC value with format yyyy-mm-ddThh:mm:ss, representing when the scan finished.",
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}\\:\\d{2}\\:\\d{2}$",
+ "examples": [
+ "2020-01-28T03:26:02"
+ ]
+ },
+ "messages": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "description": "Communication intended for the initiator of a scan.",
+ "required": [
+ "level",
+ "value"
+ ],
+ "properties": {
+ "level": {
+ "type": "string",
+ "description": "Describes the severity of the communication. Use info to communicate normal scan behaviour; warn to communicate a potentially recoverable problem, or a partial error; fatal to communicate an issue that causes the scan to halt.",
+ "enum": [
+ "info",
+ "warn",
+ "fatal"
+ ],
+ "examples": [
+ "info"
+ ]
+ },
+ "value": {
+ "type": "string",
+ "description": "The message to communicate.",
+ "minLength": 1,
+ "examples": [
+ "Permission denied, scanning aborted"
+ ]
+ }
+ }
+ }
+ },
+ "analyzer": {
+ "type": "object",
+ "description": "Object defining the analyzer used to perform the scan. Analyzers typically delegate to an underlying scanner to run the scan.",
+ "required": [
+ "id",
+ "name",
+ "version",
+ "vendor"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Unique id that identifies the analyzer.",
+ "minLength": 1,
+ "examples": [
+ "gitlab-dast"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "description": "A human readable value that identifies the analyzer, not required to be unique.",
+ "minLength": 1,
+ "examples": [
+ "GitLab DAST"
+ ]
+ },
+ "url": {
+ "type": "string",
+ "format": "uri",
+ "pattern": "^https?://.+",
+ "description": "A link to more information about the analyzer.",
+ "examples": [
+ "https://docs.gitlab.com/ee/user/application_security/dast"
+ ]
+ },
+ "vendor": {
+ "description": "The vendor/maintainer of the analyzer.",
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The name of the vendor.",
+ "minLength": 1,
+ "examples": [
+ "GitLab"
+ ]
+ }
+ }
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the analyzer.",
+ "minLength": 1,
+ "examples": [
+ "1.0.2"
+ ]
+ }
+ }
+ },
+ "scanner": {
+ "type": "object",
+ "description": "Object defining the scanner used to perform the scan.",
+ "required": [
+ "id",
+ "name",
+ "version",
+ "vendor"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Unique id that identifies the scanner.",
+ "minLength": 1,
+ "examples": [
+ "my-sast-scanner"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "description": "A human readable value that identifies the scanner, not required to be unique.",
+ "minLength": 1,
+ "examples": [
+ "My SAST Scanner"
+ ]
+ },
+ "url": {
+ "type": "string",
+ "description": "A link to more information about the scanner.",
+ "examples": [
+ "https://scanner.url"
+ ]
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the scanner.",
+ "minLength": 1,
+ "examples": [
+ "1.0.2"
+ ]
+ },
+ "vendor": {
+ "description": "The vendor/maintainer of the scanner.",
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The name of the vendor.",
+ "minLength": 1,
+ "examples": [
+ "GitLab"
+ ]
+ }
+ }
+ }
+ }
+ },
+ "start_time": {
+ "type": "string",
+ "description": "ISO8601 UTC value with format yyyy-mm-ddThh:mm:ss, representing when the scan started.",
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}\\:\\d{2}\\:\\d{2}$",
+ "examples": [
+ "2020-02-14T16:01:59"
+ ]
+ },
+ "status": {
+ "type": "string",
+ "description": "Result of the scan.",
+ "enum": [
+ "success",
+ "failure"
+ ]
+ },
+ "type": {
+ "type": "string",
+ "description": "Type of the scan.",
+ "enum": [
+ "coverage_fuzzing"
+ ]
+ }
+ }
+ },
+ "schema": {
+ "type": "string",
+ "description": "URI pointing to the validating security report schema.",
+ "format": "uri"
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the schema to which the JSON report conforms.",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ },
+ "vulnerabilities": {
+ "type": "array",
+ "description": "Array of vulnerability objects.",
+ "items": {
+ "type": "object",
+ "description": "Describes the vulnerability using GitLab Flavored Markdown",
+ "required": [
+ "category",
+ "cve",
+ "identifiers",
+ "location",
+ "scanner"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Unique identifier of the vulnerability. This is recommended to be a UUID.",
+ "examples": [
+ "642735a5-1425-428d-8d4e-3c854885a3c9"
+ ]
+ },
+ "category": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Describes where this vulnerability belongs (for example, SAST, Dependency Scanning, and so on)."
+ },
+ "name": {
+ "type": "string",
+ "description": "The name of the vulnerability. This must not include the finding's specific information."
+ },
+ "message": {
+ "type": "string",
+ "description": "A short text section that describes the vulnerability. This may include the finding's specific information."
+ },
+ "description": {
+ "type": "string",
+ "description": "A long text section describing the vulnerability more fully."
+ },
+ "cve": {
+ "type": "string",
+ "description": "(Deprecated - use vulnerabilities[].id instead) A fingerprint string value that represents a concrete finding. This is used to determine whether two findings are same, which may not be 100% accurate. Note that this is NOT a CVE as described by https://cve.mitre.org/."
+ },
+ "severity": {
+ "type": "string",
+ "description": "How much the vulnerability impacts the software. Possible values are Info, Unknown, Low, Medium, High, or Critical. Note that some analyzers may not report all these possible values.",
+ "enum": [
+ "Info",
+ "Unknown",
+ "Low",
+ "Medium",
+ "High",
+ "Critical"
+ ]
+ },
+ "confidence": {
+ "type": "string",
+ "description": "How reliable the vulnerability's assessment is. Possible values are Ignore, Unknown, Experimental, Low, Medium, High, and Confirmed. Note that some analyzers may not report all these possible values.",
+ "enum": [
+ "Ignore",
+ "Unknown",
+ "Experimental",
+ "Low",
+ "Medium",
+ "High",
+ "Confirmed"
+ ]
+ },
+ "solution": {
+ "type": "string",
+ "description": "Explanation of how to fix the vulnerability."
+ },
+ "scanner": {
+ "description": "Describes the scanner used to find this vulnerability.",
+ "type": "object",
+ "required": [
+ "id",
+ "name"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "minLength": 1,
+ "description": "The scanner's ID, as a snake_case string."
+ },
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Human-readable name of the scanner."
+ }
+ }
+ },
+ "identifiers": {
+ "type": "array",
+ "minItems": 1,
+ "description": "An ordered array of references that identify a vulnerability on internal or external databases. The first identifier is the Primary Identifier, which has special meaning.",
+ "items": {
+ "type": "object",
+ "required": [
+ "type",
+ "name",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "for example, cve, cwe, osvdb, usn, or an analyzer-dependent type such as gemnasium).",
+ "minLength": 1
+ },
+ "name": {
+ "type": "string",
+ "description": "Human-readable name of the identifier.",
+ "minLength": 1
+ },
+ "url": {
+ "type": "string",
+ "description": "URL of the identifier's documentation.",
+ "format": "uri"
+ },
+ "value": {
+ "type": "string",
+ "description": "Value of the identifier, for matching purpose.",
+ "minLength": 1
+ }
+ }
+ }
+ },
+ "links": {
+ "type": "array",
+ "description": "An array of references to external documentation or articles that describe the vulnerability.",
+ "items": {
+ "type": "object",
+ "required": [
+ "url"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Name of the vulnerability details link."
+ },
+ "url": {
+ "type": "string",
+ "description": "URL of the vulnerability details document.",
+ "format": "uri"
+ }
+ }
+ }
+ },
+ "details": {
+ "$ref": "#/definitions/named_list/properties/items"
+ },
+ "tracking": {
+ "description": "Describes how this vulnerability should be tracked as the project changes.",
+ "oneOf": [
+ {
+ "description": "Declares that a series of items should be tracked using source-specific tracking methods.",
+ "required": [
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "source"
+ },
+ "items": {
+ "type": "array",
+ "items": {
+ "description": "An item that should be tracked using source-specific tracking methods.",
+ "type": "object",
+ "required": [
+ "signatures"
+ ],
+ "properties": {
+ "file": {
+ "type": "string",
+ "description": "Path to the file where the vulnerability is located."
+ },
+ "start_line": {
+ "type": "number",
+ "description": "The first line of the file that includes the vulnerability."
+ },
+ "end_line": {
+ "type": "number",
+ "description": "The last line of the file that includes the vulnerability."
+ },
+ "signatures": {
+ "type": "array",
+ "description": "An array of calculated tracking signatures for this tracking item.",
+ "minItems": 1,
+ "items": {
+ "description": "A calculated tracking signature value and metadata.",
+ "required": [
+ "algorithm",
+ "value"
+ ],
+ "properties": {
+ "algorithm": {
+ "type": "string",
+ "description": "The algorithm used to generate the signature."
+ },
+ "value": {
+ "type": "string",
+ "description": "The result of this signature algorithm."
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "Each tracking type must declare its own type."
+ }
+ }
+ },
+ "flags": {
+ "description": "Flags that can be attached to vulnerabilities.",
+ "type": "array",
+ "items": {
+ "type": "object",
+ "description": "Informational flags identified and assigned to a vulnerability.",
+ "required": [
+ "type",
+ "origin",
+ "description"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Result of the scan.",
+ "enum": [
+ "flagged-as-likely-false-positive"
+ ]
+ },
+ "origin": {
+ "minLength": 1,
+ "description": "Tool that issued the flag.",
+ "type": "string"
+ },
+ "description": {
+ "minLength": 1,
+ "description": "What the flag is about.",
+ "type": "string"
+ }
+ }
+ }
+ },
+ "location": {
+ "description": "The location of the error",
+ "type": "object",
+ "properties": {
+ "crash_address": {
+ "type": "string",
+ "description": "The relative address in memory were the crash occurred.",
+ "examples": [
+ "0xabababab"
+ ]
+ },
+ "stacktrace_snippet": {
+ "type": "string",
+ "description": "The stack trace recorded during fuzzing resulting the crash.",
+ "examples": [
+ "func_a+0xabcd\nfunc_b+0xabcc"
+ ]
+ },
+ "crash_state": {
+ "type": "string",
+ "description": "Minimised and normalized crash stack-trace (called crash_state).",
+ "examples": [
+ "func_a+0xa\nfunc_b+0xb\nfunc_c+0xc"
+ ]
+ },
+ "crash_type": {
+ "type": "string",
+ "description": "Type of the crash.",
+ "examples": [
+ "Heap-Buffer-overflow",
+ "Division-by-zero"
+ ]
+ }
+ }
+ }
+ }
+ }
+ },
+ "remediations": {
+ "type": "array",
+ "description": "An array of objects containing information on available remediations, along with patch diffs to apply.",
+ "items": {
+ "type": "object",
+ "required": [
+ "fixes",
+ "summary",
+ "diff"
+ ],
+ "properties": {
+ "fixes": {
+ "type": "array",
+ "description": "An array of strings that represent references to vulnerabilities fixed by this remediation.",
+ "items": {
+ "type": "object",
+ "required": [
+ "cve"
+ ],
+ "properties": {
+ "cve": {
+ "type": "string",
+ "description": "(Deprecated - use vulnerabilities[].id instead) A fingerprint string value that represents a concrete finding. This is used to determine whether two findings are same, which may not be 100% accurate. Note that this is NOT a CVE as described by https://cve.mitre.org/."
+ }
+ }
+ }
+ },
+ "summary": {
+ "type": "string",
+ "minLength": 1,
+ "description": "An overview of how the vulnerabilities were fixed."
+ },
+ "diff": {
+ "type": "string",
+ "minLength": 1,
+ "description": "A base64-encoded remediation code diff, compatible with git apply."
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/lib/gitlab/ci/parsers/security/validators/schemas/14.1.3/dast-report-format.json b/lib/gitlab/ci/parsers/security/validators/schemas/14.1.3/dast-report-format.json
new file mode 100644
index 00000000000..0e4c866794a
--- /dev/null
+++ b/lib/gitlab/ci/parsers/security/validators/schemas/14.1.3/dast-report-format.json
@@ -0,0 +1,1287 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "title": "Report format for GitLab DAST",
+ "description": "This schema provides the the report format for Dynamic Application Security Testing (https://docs.gitlab.com/ee/user/application_security/dast).",
+ "definitions": {
+ "detail_type": {
+ "oneOf": [
+ {
+ "$ref": "#/definitions/named_list"
+ },
+ {
+ "$ref": "#/definitions/list"
+ },
+ {
+ "$ref": "#/definitions/table"
+ },
+ {
+ "$ref": "#/definitions/text"
+ },
+ {
+ "$ref": "#/definitions/url"
+ },
+ {
+ "$ref": "#/definitions/code"
+ },
+ {
+ "$ref": "#/definitions/value"
+ },
+ {
+ "$ref": "#/definitions/diff"
+ },
+ {
+ "$ref": "#/definitions/markdown"
+ },
+ {
+ "$ref": "#/definitions/commit"
+ },
+ {
+ "$ref": "#/definitions/file_location"
+ },
+ {
+ "$ref": "#/definitions/module_location"
+ }
+ ]
+ },
+ "text_value": {
+ "type": "string"
+ },
+ "named_field": {
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "$ref": "#/definitions/text_value",
+ "minLength": 1
+ },
+ "description": {
+ "$ref": "#/definitions/text_value"
+ }
+ }
+ },
+ "named_list": {
+ "type": "object",
+ "description": "An object with named and typed fields",
+ "required": [
+ "type",
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "named-list"
+ },
+ "items": {
+ "type": "object",
+ "patternProperties": {
+ "^.*$": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/named_field"
+ },
+ {
+ "$ref": "#/definitions/detail_type"
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "list": {
+ "type": "object",
+ "description": "A list of typed fields",
+ "required": [
+ "type",
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "list"
+ },
+ "items": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ }
+ }
+ },
+ "table": {
+ "type": "object",
+ "description": "A table of typed fields",
+ "required": [
+ "type",
+ "rows"
+ ],
+ "properties": {
+ "type": {
+ "const": "table"
+ },
+ "header": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ },
+ "rows": {
+ "type": "array",
+ "items": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ }
+ }
+ }
+ },
+ "text": {
+ "type": "object",
+ "description": "Raw text",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "text"
+ },
+ "value": {
+ "$ref": "#/definitions/text_value"
+ }
+ }
+ },
+ "url": {
+ "type": "object",
+ "description": "A single URL",
+ "required": [
+ "type",
+ "href"
+ ],
+ "properties": {
+ "type": {
+ "const": "url"
+ },
+ "text": {
+ "$ref": "#/definitions/text_value"
+ },
+ "href": {
+ "type": "string",
+ "minLength": 1,
+ "examples": [
+ "http://mysite.com"
+ ]
+ }
+ }
+ },
+ "code": {
+ "type": "object",
+ "description": "A codeblock",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "code"
+ },
+ "value": {
+ "type": "string"
+ },
+ "lang": {
+ "type": "string",
+ "description": "A programming language"
+ }
+ }
+ },
+ "value": {
+ "type": "object",
+ "description": "A field that can store a range of types of value",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "value"
+ },
+ "value": {
+ "type": [
+ "number",
+ "string",
+ "boolean"
+ ]
+ }
+ }
+ },
+ "diff": {
+ "type": "object",
+ "description": "A diff",
+ "required": [
+ "type",
+ "before",
+ "after"
+ ],
+ "properties": {
+ "type": {
+ "const": "diff"
+ },
+ "before": {
+ "type": "string"
+ },
+ "after": {
+ "type": "string"
+ }
+ }
+ },
+ "markdown": {
+ "type": "object",
+ "description": "GitLab flavoured markdown, see https://docs.gitlab.com/ee/user/markdown.html",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "markdown"
+ },
+ "value": {
+ "$ref": "#/definitions/text_value",
+ "examples": [
+ "Here is markdown `inline code` #1 [test](gitlab.com)\n\n![GitLab Logo](https://about.gitlab.com/images/press/logo/preview/gitlab-logo-white-preview.png)"
+ ]
+ }
+ }
+ },
+ "commit": {
+ "type": "object",
+ "description": "A commit/tag/branch within the GitLab project",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "commit"
+ },
+ "value": {
+ "type": "string",
+ "description": "The commit SHA",
+ "minLength": 1
+ }
+ }
+ },
+ "file_location": {
+ "type": "object",
+ "description": "A location within a file in the project",
+ "required": [
+ "type",
+ "file_name",
+ "line_start"
+ ],
+ "properties": {
+ "type": {
+ "const": "file-location"
+ },
+ "file_name": {
+ "type": "string",
+ "minLength": 1
+ },
+ "line_start": {
+ "type": "integer"
+ },
+ "line_end": {
+ "type": "integer"
+ }
+ }
+ },
+ "module_location": {
+ "type": "object",
+ "description": "A location within a binary module of the form module+relative_offset",
+ "required": [
+ "type",
+ "module_name",
+ "offset"
+ ],
+ "properties": {
+ "type": {
+ "const": "module-location"
+ },
+ "module_name": {
+ "type": "string",
+ "minLength": 1,
+ "examples": [
+ "compiled_binary"
+ ]
+ },
+ "offset": {
+ "type": "integer",
+ "examples": [
+ 100
+ ]
+ }
+ }
+ }
+ },
+ "self": {
+ "version": "14.1.3"
+ },
+ "required": [
+ "version",
+ "vulnerabilities"
+ ],
+ "additionalProperties": true,
+ "properties": {
+ "scan": {
+ "type": "object",
+ "required": [
+ "end_time",
+ "scanned_resources",
+ "scanner",
+ "start_time",
+ "status",
+ "type"
+ ],
+ "properties": {
+ "end_time": {
+ "type": "string",
+ "description": "ISO8601 UTC value with format yyyy-mm-ddThh:mm:ss, representing when the scan finished.",
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}\\:\\d{2}\\:\\d{2}$",
+ "examples": [
+ "2020-01-28T03:26:02"
+ ]
+ },
+ "messages": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "description": "Communication intended for the initiator of a scan.",
+ "required": [
+ "level",
+ "value"
+ ],
+ "properties": {
+ "level": {
+ "type": "string",
+ "description": "Describes the severity of the communication. Use info to communicate normal scan behaviour; warn to communicate a potentially recoverable problem, or a partial error; fatal to communicate an issue that causes the scan to halt.",
+ "enum": [
+ "info",
+ "warn",
+ "fatal"
+ ],
+ "examples": [
+ "info"
+ ]
+ },
+ "value": {
+ "type": "string",
+ "description": "The message to communicate.",
+ "minLength": 1,
+ "examples": [
+ "Permission denied, scanning aborted"
+ ]
+ }
+ }
+ }
+ },
+ "analyzer": {
+ "type": "object",
+ "description": "Object defining the analyzer used to perform the scan. Analyzers typically delegate to an underlying scanner to run the scan.",
+ "required": [
+ "id",
+ "name",
+ "version",
+ "vendor"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Unique id that identifies the analyzer.",
+ "minLength": 1,
+ "examples": [
+ "gitlab-dast"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "description": "A human readable value that identifies the analyzer, not required to be unique.",
+ "minLength": 1,
+ "examples": [
+ "GitLab DAST"
+ ]
+ },
+ "url": {
+ "type": "string",
+ "format": "uri",
+ "pattern": "^https?://.+",
+ "description": "A link to more information about the analyzer.",
+ "examples": [
+ "https://docs.gitlab.com/ee/user/application_security/dast"
+ ]
+ },
+ "vendor": {
+ "description": "The vendor/maintainer of the analyzer.",
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The name of the vendor.",
+ "minLength": 1,
+ "examples": [
+ "GitLab"
+ ]
+ }
+ }
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the analyzer.",
+ "minLength": 1,
+ "examples": [
+ "1.0.2"
+ ]
+ }
+ }
+ },
+ "scanner": {
+ "type": "object",
+ "description": "Object defining the scanner used to perform the scan.",
+ "required": [
+ "id",
+ "name",
+ "version",
+ "vendor"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Unique id that identifies the scanner.",
+ "minLength": 1,
+ "examples": [
+ "my-sast-scanner"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "description": "A human readable value that identifies the scanner, not required to be unique.",
+ "minLength": 1,
+ "examples": [
+ "My SAST Scanner"
+ ]
+ },
+ "url": {
+ "type": "string",
+ "description": "A link to more information about the scanner.",
+ "examples": [
+ "https://scanner.url"
+ ]
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the scanner.",
+ "minLength": 1,
+ "examples": [
+ "1.0.2"
+ ]
+ },
+ "vendor": {
+ "description": "The vendor/maintainer of the scanner.",
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The name of the vendor.",
+ "minLength": 1,
+ "examples": [
+ "GitLab"
+ ]
+ }
+ }
+ }
+ }
+ },
+ "start_time": {
+ "type": "string",
+ "description": "ISO8601 UTC value with format yyyy-mm-ddThh:mm:ss, representing when the scan started.",
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}\\:\\d{2}\\:\\d{2}$",
+ "examples": [
+ "2020-02-14T16:01:59"
+ ]
+ },
+ "status": {
+ "type": "string",
+ "description": "Result of the scan.",
+ "enum": [
+ "success",
+ "failure"
+ ]
+ },
+ "type": {
+ "type": "string",
+ "description": "Type of the scan.",
+ "enum": [
+ "dast",
+ "api_fuzzing"
+ ]
+ },
+ "scanned_resources": {
+ "type": "array",
+ "description": "The attack surface scanned by DAST.",
+ "items": {
+ "type": "object",
+ "required": [
+ "method",
+ "url",
+ "type"
+ ],
+ "properties": {
+ "method": {
+ "type": "string",
+ "minLength": 1,
+ "description": "HTTP method of the scanned resource.",
+ "examples": [
+ "GET",
+ "POST",
+ "HEAD"
+ ]
+ },
+ "url": {
+ "type": "string",
+ "minLength": 1,
+ "description": "URL of the scanned resource.",
+ "examples": [
+ "http://my.site.com/a-page"
+ ]
+ },
+ "type": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Type of the scanned resource, for DAST, this must be 'url'.",
+ "examples": [
+ "url"
+ ]
+ }
+ }
+ }
+ }
+ }
+ },
+ "schema": {
+ "type": "string",
+ "description": "URI pointing to the validating security report schema.",
+ "format": "uri"
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the schema to which the JSON report conforms.",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ },
+ "vulnerabilities": {
+ "type": "array",
+ "description": "Array of vulnerability objects.",
+ "items": {
+ "type": "object",
+ "description": "Describes the vulnerability using GitLab Flavored Markdown",
+ "required": [
+ "category",
+ "cve",
+ "identifiers",
+ "location",
+ "scanner"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Unique identifier of the vulnerability. This is recommended to be a UUID.",
+ "examples": [
+ "642735a5-1425-428d-8d4e-3c854885a3c9"
+ ]
+ },
+ "category": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Describes where this vulnerability belongs (for example, SAST, Dependency Scanning, and so on)."
+ },
+ "name": {
+ "type": "string",
+ "description": "The name of the vulnerability. This must not include the finding's specific information."
+ },
+ "message": {
+ "type": "string",
+ "description": "A short text section that describes the vulnerability. This may include the finding's specific information."
+ },
+ "description": {
+ "type": "string",
+ "description": "A long text section describing the vulnerability more fully."
+ },
+ "cve": {
+ "type": "string",
+ "description": "(Deprecated - use vulnerabilities[].id instead) A fingerprint string value that represents a concrete finding. This is used to determine whether two findings are same, which may not be 100% accurate. Note that this is NOT a CVE as described by https://cve.mitre.org/."
+ },
+ "severity": {
+ "type": "string",
+ "description": "How much the vulnerability impacts the software. Possible values are Info, Unknown, Low, Medium, High, or Critical. Note that some analyzers may not report all these possible values.",
+ "enum": [
+ "Info",
+ "Unknown",
+ "Low",
+ "Medium",
+ "High",
+ "Critical"
+ ]
+ },
+ "confidence": {
+ "type": "string",
+ "description": "How reliable the vulnerability's assessment is. Possible values are Ignore, Unknown, Experimental, Low, Medium, High, and Confirmed. Note that some analyzers may not report all these possible values.",
+ "enum": [
+ "Ignore",
+ "Unknown",
+ "Experimental",
+ "Low",
+ "Medium",
+ "High",
+ "Confirmed"
+ ]
+ },
+ "solution": {
+ "type": "string",
+ "description": "Explanation of how to fix the vulnerability."
+ },
+ "scanner": {
+ "description": "Describes the scanner used to find this vulnerability.",
+ "type": "object",
+ "required": [
+ "id",
+ "name"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "minLength": 1,
+ "description": "The scanner's ID, as a snake_case string."
+ },
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Human-readable name of the scanner."
+ }
+ }
+ },
+ "identifiers": {
+ "type": "array",
+ "minItems": 1,
+ "description": "An ordered array of references that identify a vulnerability on internal or external databases. The first identifier is the Primary Identifier, which has special meaning.",
+ "items": {
+ "type": "object",
+ "required": [
+ "type",
+ "name",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "for example, cve, cwe, osvdb, usn, or an analyzer-dependent type such as gemnasium).",
+ "minLength": 1
+ },
+ "name": {
+ "type": "string",
+ "description": "Human-readable name of the identifier.",
+ "minLength": 1
+ },
+ "url": {
+ "type": "string",
+ "description": "URL of the identifier's documentation.",
+ "format": "uri"
+ },
+ "value": {
+ "type": "string",
+ "description": "Value of the identifier, for matching purpose.",
+ "minLength": 1
+ }
+ }
+ }
+ },
+ "links": {
+ "type": "array",
+ "description": "An array of references to external documentation or articles that describe the vulnerability.",
+ "items": {
+ "type": "object",
+ "required": [
+ "url"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Name of the vulnerability details link."
+ },
+ "url": {
+ "type": "string",
+ "description": "URL of the vulnerability details document.",
+ "format": "uri"
+ }
+ }
+ }
+ },
+ "details": {
+ "$ref": "#/definitions/named_list/properties/items"
+ },
+ "tracking": {
+ "description": "Describes how this vulnerability should be tracked as the project changes.",
+ "oneOf": [
+ {
+ "description": "Declares that a series of items should be tracked using source-specific tracking methods.",
+ "required": [
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "source"
+ },
+ "items": {
+ "type": "array",
+ "items": {
+ "description": "An item that should be tracked using source-specific tracking methods.",
+ "type": "object",
+ "required": [
+ "signatures"
+ ],
+ "properties": {
+ "file": {
+ "type": "string",
+ "description": "Path to the file where the vulnerability is located."
+ },
+ "start_line": {
+ "type": "number",
+ "description": "The first line of the file that includes the vulnerability."
+ },
+ "end_line": {
+ "type": "number",
+ "description": "The last line of the file that includes the vulnerability."
+ },
+ "signatures": {
+ "type": "array",
+ "description": "An array of calculated tracking signatures for this tracking item.",
+ "minItems": 1,
+ "items": {
+ "description": "A calculated tracking signature value and metadata.",
+ "required": [
+ "algorithm",
+ "value"
+ ],
+ "properties": {
+ "algorithm": {
+ "type": "string",
+ "description": "The algorithm used to generate the signature."
+ },
+ "value": {
+ "type": "string",
+ "description": "The result of this signature algorithm."
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "Each tracking type must declare its own type."
+ }
+ }
+ },
+ "flags": {
+ "description": "Flags that can be attached to vulnerabilities.",
+ "type": "array",
+ "items": {
+ "type": "object",
+ "description": "Informational flags identified and assigned to a vulnerability.",
+ "required": [
+ "type",
+ "origin",
+ "description"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Result of the scan.",
+ "enum": [
+ "flagged-as-likely-false-positive"
+ ]
+ },
+ "origin": {
+ "minLength": 1,
+ "description": "Tool that issued the flag.",
+ "type": "string"
+ },
+ "description": {
+ "minLength": 1,
+ "description": "What the flag is about.",
+ "type": "string"
+ }
+ }
+ }
+ },
+ "evidence": {
+ "type": "object",
+ "properties": {
+ "source": {
+ "type": "object",
+ "description": "Source of evidence",
+ "required": [
+ "id",
+ "name"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Unique source identifier",
+ "examples": [
+ "assert:LogAnalysis",
+ "assert:StatusCode"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Source display name",
+ "examples": [
+ "Log Analysis",
+ "Status Code"
+ ]
+ },
+ "url": {
+ "type": "string",
+ "description": "Link to additional information",
+ "examples": [
+ "https://docs.gitlab.com/ee/development/integrations/secure.html"
+ ]
+ }
+ }
+ },
+ "summary": {
+ "type": "string",
+ "description": "Human readable string containing evidence of the vulnerability.",
+ "examples": [
+ "Credit card 4111111111111111 found",
+ "Server leaked information nginx/1.17.6"
+ ]
+ },
+ "request": {
+ "type": "object",
+ "description": "An HTTP request.",
+ "required": [
+ "headers",
+ "method",
+ "url"
+ ],
+ "properties": {
+ "headers": {
+ "type": "array",
+ "description": "HTTP headers present on the request.",
+ "items": {
+ "type": "object",
+ "required": [
+ "name",
+ "value"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Name of the HTTP header.",
+ "examples": [
+ "Accept",
+ "Content-Length",
+ "Content-Type"
+ ]
+ },
+ "value": {
+ "type": "string",
+ "description": "Value of the HTTP header.",
+ "examples": [
+ "*/*",
+ "560",
+ "application/json; charset=utf-8"
+ ]
+ }
+ }
+ }
+ },
+ "method": {
+ "type": "string",
+ "minLength": 1,
+ "description": "HTTP method used in the request.",
+ "examples": [
+ "GET",
+ "POST"
+ ]
+ },
+ "url": {
+ "type": "string",
+ "minLength": 1,
+ "description": "URL of the request.",
+ "examples": [
+ "http://my.site.com/vulnerable-endpoint?show-credit-card"
+ ]
+ },
+ "body": {
+ "type": "string",
+ "description": "Body of the request for display purposes. Body must be suitable for display (not binary), and truncated to a reasonable size.",
+ "examples": [
+ "user=jsmith&first=%27&last=smith"
+ ]
+ }
+ }
+ },
+ "response": {
+ "type": "object",
+ "description": "An HTTP response.",
+ "required": [
+ "headers",
+ "reason_phrase",
+ "status_code"
+ ],
+ "properties": {
+ "headers": {
+ "type": "array",
+ "description": "HTTP headers present on the request.",
+ "items": {
+ "type": "object",
+ "required": [
+ "name",
+ "value"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Name of the HTTP header.",
+ "examples": [
+ "Accept",
+ "Content-Length",
+ "Content-Type"
+ ]
+ },
+ "value": {
+ "type": "string",
+ "description": "Value of the HTTP header.",
+ "examples": [
+ "*/*",
+ "560",
+ "application/json; charset=utf-8"
+ ]
+ }
+ }
+ }
+ },
+ "reason_phrase": {
+ "type": "string",
+ "description": "HTTP reason phrase of the response.",
+ "examples": [
+ "OK",
+ "Internal Server Error"
+ ]
+ },
+ "status_code": {
+ "type": "integer",
+ "description": "HTTP status code of the response.",
+ "examples": [
+ 200,
+ 500
+ ]
+ },
+ "body": {
+ "type": "string",
+ "description": "Body of the response for display purposes. Body must be suitable for display (not binary), and truncated to a reasonable size.",
+ "examples": [
+ "{\"user_id\": 2}"
+ ]
+ }
+ }
+ },
+ "supporting_messages": {
+ "type": "array",
+ "description": "Array of supporting http messages.",
+ "items": {
+ "type": "object",
+ "description": "A supporting http message.",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Message display name.",
+ "examples": [
+ "Unmodified",
+ "Recorded"
+ ]
+ },
+ "request": {
+ "type": "object",
+ "description": "An HTTP request.",
+ "required": [
+ "headers",
+ "method",
+ "url"
+ ],
+ "properties": {
+ "headers": {
+ "type": "array",
+ "description": "HTTP headers present on the request.",
+ "items": {
+ "type": "object",
+ "required": [
+ "name",
+ "value"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Name of the HTTP header.",
+ "examples": [
+ "Accept",
+ "Content-Length",
+ "Content-Type"
+ ]
+ },
+ "value": {
+ "type": "string",
+ "description": "Value of the HTTP header.",
+ "examples": [
+ "*/*",
+ "560",
+ "application/json; charset=utf-8"
+ ]
+ }
+ }
+ }
+ },
+ "method": {
+ "type": "string",
+ "minLength": 1,
+ "description": "HTTP method used in the request.",
+ "examples": [
+ "GET",
+ "POST"
+ ]
+ },
+ "url": {
+ "type": "string",
+ "minLength": 1,
+ "description": "URL of the request.",
+ "examples": [
+ "http://my.site.com/vulnerable-endpoint?show-credit-card"
+ ]
+ },
+ "body": {
+ "type": "string",
+ "description": "Body of the request for display purposes. Body must be suitable for display (not binary), and truncated to a reasonable size.",
+ "examples": [
+ "user=jsmith&first=%27&last=smith"
+ ]
+ }
+ }
+ },
+ "response": {
+ "type": "object",
+ "description": "An HTTP response.",
+ "required": [
+ "headers",
+ "reason_phrase",
+ "status_code"
+ ],
+ "properties": {
+ "headers": {
+ "type": "array",
+ "description": "HTTP headers present on the request.",
+ "items": {
+ "type": "object",
+ "required": [
+ "name",
+ "value"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Name of the HTTP header.",
+ "examples": [
+ "Accept",
+ "Content-Length",
+ "Content-Type"
+ ]
+ },
+ "value": {
+ "type": "string",
+ "description": "Value of the HTTP header.",
+ "examples": [
+ "*/*",
+ "560",
+ "application/json; charset=utf-8"
+ ]
+ }
+ }
+ }
+ },
+ "reason_phrase": {
+ "type": "string",
+ "description": "HTTP reason phrase of the response.",
+ "examples": [
+ "OK",
+ "Internal Server Error"
+ ]
+ },
+ "status_code": {
+ "type": "integer",
+ "description": "HTTP status code of the response.",
+ "examples": [
+ 200,
+ 500
+ ]
+ },
+ "body": {
+ "type": "string",
+ "description": "Body of the response for display purposes. Body must be suitable for display (not binary), and truncated to a reasonable size.",
+ "examples": [
+ "{\"user_id\": 2}"
+ ]
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "location": {
+ "type": "object",
+ "description": "Identifies the vulnerability's location.",
+ "properties": {
+ "hostname": {
+ "type": "string",
+ "description": "The protocol, domain, and port of the application where the vulnerability was found."
+ },
+ "method": {
+ "type": "string",
+ "description": "The HTTP method that was used to request the URL where the vulnerability was found."
+ },
+ "param": {
+ "type": "string",
+ "description": "A value provided by a vulnerability rule related to the found vulnerability. Examples include a header value, or a parameter used in a HTTP POST."
+ },
+ "path": {
+ "type": "string",
+ "description": "The path of the URL where the vulnerability was found. Typically, this would start with a forward slash."
+ }
+ }
+ },
+ "assets": {
+ "type": "array",
+ "description": "Array of build assets associated with vulnerability.",
+ "items": {
+ "type": "object",
+ "description": "Describes an asset associated with vulnerability.",
+ "required": [
+ "type",
+ "name",
+ "url"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "The type of asset",
+ "enum": [
+ "http_session",
+ "postman"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Display name for asset",
+ "examples": [
+ "HTTP Messages",
+ "Postman Collection"
+ ]
+ },
+ "url": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Link to asset in build artifacts",
+ "examples": [
+ "https://gitlab.com/gitlab-org/security-products/dast/-/jobs/626397001/artifacts/file//output/zap_session.data"
+ ]
+ }
+ }
+ }
+ },
+ "discovered_at": {
+ "type": "string",
+ "description": "ISO8601 UTC value with format yyyy-mm-ddThh:mm:ss.sss, representing when the vulnerability was discovered",
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}\\:\\d{2}\\:\\d{2}\\.\\d{3}$",
+ "examples": [
+ "2020-01-28T03:26:02.956"
+ ]
+ }
+ }
+ }
+ },
+ "remediations": {
+ "type": "array",
+ "description": "An array of objects containing information on available remediations, along with patch diffs to apply.",
+ "items": {
+ "type": "object",
+ "required": [
+ "fixes",
+ "summary",
+ "diff"
+ ],
+ "properties": {
+ "fixes": {
+ "type": "array",
+ "description": "An array of strings that represent references to vulnerabilities fixed by this remediation.",
+ "items": {
+ "type": "object",
+ "required": [
+ "cve"
+ ],
+ "properties": {
+ "cve": {
+ "type": "string",
+ "description": "(Deprecated - use vulnerabilities[].id instead) A fingerprint string value that represents a concrete finding. This is used to determine whether two findings are same, which may not be 100% accurate. Note that this is NOT a CVE as described by https://cve.mitre.org/."
+ }
+ }
+ }
+ },
+ "summary": {
+ "type": "string",
+ "minLength": 1,
+ "description": "An overview of how the vulnerabilities were fixed."
+ },
+ "diff": {
+ "type": "string",
+ "minLength": 1,
+ "description": "A base64-encoded remediation code diff, compatible with git apply."
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/lib/gitlab/ci/parsers/security/validators/schemas/14.1.3/dependency-scanning-report-format.json b/lib/gitlab/ci/parsers/security/validators/schemas/14.1.3/dependency-scanning-report-format.json
new file mode 100644
index 00000000000..652c2f48fe4
--- /dev/null
+++ b/lib/gitlab/ci/parsers/security/validators/schemas/14.1.3/dependency-scanning-report-format.json
@@ -0,0 +1,968 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "title": "Report format for GitLab Dependency Scanning",
+ "description": "This schema provides the the report format for Dependency Scanning analyzers (https://docs.gitlab.com/ee/user/application_security/dependency_scanning).",
+ "definitions": {
+ "detail_type": {
+ "oneOf": [
+ {
+ "$ref": "#/definitions/named_list"
+ },
+ {
+ "$ref": "#/definitions/list"
+ },
+ {
+ "$ref": "#/definitions/table"
+ },
+ {
+ "$ref": "#/definitions/text"
+ },
+ {
+ "$ref": "#/definitions/url"
+ },
+ {
+ "$ref": "#/definitions/code"
+ },
+ {
+ "$ref": "#/definitions/value"
+ },
+ {
+ "$ref": "#/definitions/diff"
+ },
+ {
+ "$ref": "#/definitions/markdown"
+ },
+ {
+ "$ref": "#/definitions/commit"
+ },
+ {
+ "$ref": "#/definitions/file_location"
+ },
+ {
+ "$ref": "#/definitions/module_location"
+ }
+ ]
+ },
+ "text_value": {
+ "type": "string"
+ },
+ "named_field": {
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "$ref": "#/definitions/text_value",
+ "minLength": 1
+ },
+ "description": {
+ "$ref": "#/definitions/text_value"
+ }
+ }
+ },
+ "named_list": {
+ "type": "object",
+ "description": "An object with named and typed fields",
+ "required": [
+ "type",
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "named-list"
+ },
+ "items": {
+ "type": "object",
+ "patternProperties": {
+ "^.*$": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/named_field"
+ },
+ {
+ "$ref": "#/definitions/detail_type"
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "list": {
+ "type": "object",
+ "description": "A list of typed fields",
+ "required": [
+ "type",
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "list"
+ },
+ "items": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ }
+ }
+ },
+ "table": {
+ "type": "object",
+ "description": "A table of typed fields",
+ "required": [
+ "type",
+ "rows"
+ ],
+ "properties": {
+ "type": {
+ "const": "table"
+ },
+ "header": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ },
+ "rows": {
+ "type": "array",
+ "items": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ }
+ }
+ }
+ },
+ "text": {
+ "type": "object",
+ "description": "Raw text",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "text"
+ },
+ "value": {
+ "$ref": "#/definitions/text_value"
+ }
+ }
+ },
+ "url": {
+ "type": "object",
+ "description": "A single URL",
+ "required": [
+ "type",
+ "href"
+ ],
+ "properties": {
+ "type": {
+ "const": "url"
+ },
+ "text": {
+ "$ref": "#/definitions/text_value"
+ },
+ "href": {
+ "type": "string",
+ "minLength": 1,
+ "examples": [
+ "http://mysite.com"
+ ]
+ }
+ }
+ },
+ "code": {
+ "type": "object",
+ "description": "A codeblock",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "code"
+ },
+ "value": {
+ "type": "string"
+ },
+ "lang": {
+ "type": "string",
+ "description": "A programming language"
+ }
+ }
+ },
+ "value": {
+ "type": "object",
+ "description": "A field that can store a range of types of value",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "value"
+ },
+ "value": {
+ "type": [
+ "number",
+ "string",
+ "boolean"
+ ]
+ }
+ }
+ },
+ "diff": {
+ "type": "object",
+ "description": "A diff",
+ "required": [
+ "type",
+ "before",
+ "after"
+ ],
+ "properties": {
+ "type": {
+ "const": "diff"
+ },
+ "before": {
+ "type": "string"
+ },
+ "after": {
+ "type": "string"
+ }
+ }
+ },
+ "markdown": {
+ "type": "object",
+ "description": "GitLab flavoured markdown, see https://docs.gitlab.com/ee/user/markdown.html",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "markdown"
+ },
+ "value": {
+ "$ref": "#/definitions/text_value",
+ "examples": [
+ "Here is markdown `inline code` #1 [test](gitlab.com)\n\n![GitLab Logo](https://about.gitlab.com/images/press/logo/preview/gitlab-logo-white-preview.png)"
+ ]
+ }
+ }
+ },
+ "commit": {
+ "type": "object",
+ "description": "A commit/tag/branch within the GitLab project",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "commit"
+ },
+ "value": {
+ "type": "string",
+ "description": "The commit SHA",
+ "minLength": 1
+ }
+ }
+ },
+ "file_location": {
+ "type": "object",
+ "description": "A location within a file in the project",
+ "required": [
+ "type",
+ "file_name",
+ "line_start"
+ ],
+ "properties": {
+ "type": {
+ "const": "file-location"
+ },
+ "file_name": {
+ "type": "string",
+ "minLength": 1
+ },
+ "line_start": {
+ "type": "integer"
+ },
+ "line_end": {
+ "type": "integer"
+ }
+ }
+ },
+ "module_location": {
+ "type": "object",
+ "description": "A location within a binary module of the form module+relative_offset",
+ "required": [
+ "type",
+ "module_name",
+ "offset"
+ ],
+ "properties": {
+ "type": {
+ "const": "module-location"
+ },
+ "module_name": {
+ "type": "string",
+ "minLength": 1,
+ "examples": [
+ "compiled_binary"
+ ]
+ },
+ "offset": {
+ "type": "integer",
+ "examples": [
+ 100
+ ]
+ }
+ }
+ }
+ },
+ "self": {
+ "version": "14.1.3"
+ },
+ "required": [
+ "dependency_files",
+ "version",
+ "vulnerabilities"
+ ],
+ "additionalProperties": true,
+ "properties": {
+ "scan": {
+ "type": "object",
+ "required": [
+ "end_time",
+ "scanner",
+ "start_time",
+ "status",
+ "type"
+ ],
+ "properties": {
+ "end_time": {
+ "type": "string",
+ "description": "ISO8601 UTC value with format yyyy-mm-ddThh:mm:ss, representing when the scan finished.",
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}\\:\\d{2}\\:\\d{2}$",
+ "examples": [
+ "2020-01-28T03:26:02"
+ ]
+ },
+ "messages": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "description": "Communication intended for the initiator of a scan.",
+ "required": [
+ "level",
+ "value"
+ ],
+ "properties": {
+ "level": {
+ "type": "string",
+ "description": "Describes the severity of the communication. Use info to communicate normal scan behaviour; warn to communicate a potentially recoverable problem, or a partial error; fatal to communicate an issue that causes the scan to halt.",
+ "enum": [
+ "info",
+ "warn",
+ "fatal"
+ ],
+ "examples": [
+ "info"
+ ]
+ },
+ "value": {
+ "type": "string",
+ "description": "The message to communicate.",
+ "minLength": 1,
+ "examples": [
+ "Permission denied, scanning aborted"
+ ]
+ }
+ }
+ }
+ },
+ "analyzer": {
+ "type": "object",
+ "description": "Object defining the analyzer used to perform the scan. Analyzers typically delegate to an underlying scanner to run the scan.",
+ "required": [
+ "id",
+ "name",
+ "version",
+ "vendor"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Unique id that identifies the analyzer.",
+ "minLength": 1,
+ "examples": [
+ "gitlab-dast"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "description": "A human readable value that identifies the analyzer, not required to be unique.",
+ "minLength": 1,
+ "examples": [
+ "GitLab DAST"
+ ]
+ },
+ "url": {
+ "type": "string",
+ "format": "uri",
+ "pattern": "^https?://.+",
+ "description": "A link to more information about the analyzer.",
+ "examples": [
+ "https://docs.gitlab.com/ee/user/application_security/dast"
+ ]
+ },
+ "vendor": {
+ "description": "The vendor/maintainer of the analyzer.",
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The name of the vendor.",
+ "minLength": 1,
+ "examples": [
+ "GitLab"
+ ]
+ }
+ }
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the analyzer.",
+ "minLength": 1,
+ "examples": [
+ "1.0.2"
+ ]
+ }
+ }
+ },
+ "scanner": {
+ "type": "object",
+ "description": "Object defining the scanner used to perform the scan.",
+ "required": [
+ "id",
+ "name",
+ "version",
+ "vendor"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Unique id that identifies the scanner.",
+ "minLength": 1,
+ "examples": [
+ "my-sast-scanner"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "description": "A human readable value that identifies the scanner, not required to be unique.",
+ "minLength": 1,
+ "examples": [
+ "My SAST Scanner"
+ ]
+ },
+ "url": {
+ "type": "string",
+ "description": "A link to more information about the scanner.",
+ "examples": [
+ "https://scanner.url"
+ ]
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the scanner.",
+ "minLength": 1,
+ "examples": [
+ "1.0.2"
+ ]
+ },
+ "vendor": {
+ "description": "The vendor/maintainer of the scanner.",
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The name of the vendor.",
+ "minLength": 1,
+ "examples": [
+ "GitLab"
+ ]
+ }
+ }
+ }
+ }
+ },
+ "start_time": {
+ "type": "string",
+ "description": "ISO8601 UTC value with format yyyy-mm-ddThh:mm:ss, representing when the scan started.",
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}\\:\\d{2}\\:\\d{2}$",
+ "examples": [
+ "2020-02-14T16:01:59"
+ ]
+ },
+ "status": {
+ "type": "string",
+ "description": "Result of the scan.",
+ "enum": [
+ "success",
+ "failure"
+ ]
+ },
+ "type": {
+ "type": "string",
+ "description": "Type of the scan.",
+ "enum": [
+ "dependency_scanning"
+ ]
+ }
+ }
+ },
+ "schema": {
+ "type": "string",
+ "description": "URI pointing to the validating security report schema.",
+ "format": "uri"
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the schema to which the JSON report conforms.",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ },
+ "vulnerabilities": {
+ "type": "array",
+ "description": "Array of vulnerability objects.",
+ "items": {
+ "type": "object",
+ "description": "Describes the vulnerability using GitLab Flavored Markdown",
+ "required": [
+ "category",
+ "cve",
+ "identifiers",
+ "location",
+ "scanner"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Unique identifier of the vulnerability. This is recommended to be a UUID.",
+ "examples": [
+ "642735a5-1425-428d-8d4e-3c854885a3c9"
+ ]
+ },
+ "category": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Describes where this vulnerability belongs (for example, SAST, Dependency Scanning, and so on)."
+ },
+ "name": {
+ "type": "string",
+ "description": "The name of the vulnerability. This must not include the finding's specific information."
+ },
+ "message": {
+ "type": "string",
+ "description": "A short text section that describes the vulnerability. This may include the finding's specific information."
+ },
+ "description": {
+ "type": "string",
+ "description": "A long text section describing the vulnerability more fully."
+ },
+ "cve": {
+ "type": "string",
+ "description": "(Deprecated - use vulnerabilities[].id instead) A fingerprint string value that represents a concrete finding. This is used to determine whether two findings are same, which may not be 100% accurate. Note that this is NOT a CVE as described by https://cve.mitre.org/."
+ },
+ "severity": {
+ "type": "string",
+ "description": "How much the vulnerability impacts the software. Possible values are Info, Unknown, Low, Medium, High, or Critical. Note that some analyzers may not report all these possible values.",
+ "enum": [
+ "Info",
+ "Unknown",
+ "Low",
+ "Medium",
+ "High",
+ "Critical"
+ ]
+ },
+ "confidence": {
+ "type": "string",
+ "description": "How reliable the vulnerability's assessment is. Possible values are Ignore, Unknown, Experimental, Low, Medium, High, and Confirmed. Note that some analyzers may not report all these possible values.",
+ "enum": [
+ "Ignore",
+ "Unknown",
+ "Experimental",
+ "Low",
+ "Medium",
+ "High",
+ "Confirmed"
+ ]
+ },
+ "solution": {
+ "type": "string",
+ "description": "Explanation of how to fix the vulnerability."
+ },
+ "scanner": {
+ "description": "Describes the scanner used to find this vulnerability.",
+ "type": "object",
+ "required": [
+ "id",
+ "name"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "minLength": 1,
+ "description": "The scanner's ID, as a snake_case string."
+ },
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Human-readable name of the scanner."
+ }
+ }
+ },
+ "identifiers": {
+ "type": "array",
+ "minItems": 1,
+ "description": "An ordered array of references that identify a vulnerability on internal or external databases. The first identifier is the Primary Identifier, which has special meaning.",
+ "items": {
+ "type": "object",
+ "required": [
+ "type",
+ "name",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "for example, cve, cwe, osvdb, usn, or an analyzer-dependent type such as gemnasium).",
+ "minLength": 1
+ },
+ "name": {
+ "type": "string",
+ "description": "Human-readable name of the identifier.",
+ "minLength": 1
+ },
+ "url": {
+ "type": "string",
+ "description": "URL of the identifier's documentation.",
+ "format": "uri"
+ },
+ "value": {
+ "type": "string",
+ "description": "Value of the identifier, for matching purpose.",
+ "minLength": 1
+ }
+ }
+ }
+ },
+ "links": {
+ "type": "array",
+ "description": "An array of references to external documentation or articles that describe the vulnerability.",
+ "items": {
+ "type": "object",
+ "required": [
+ "url"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Name of the vulnerability details link."
+ },
+ "url": {
+ "type": "string",
+ "description": "URL of the vulnerability details document.",
+ "format": "uri"
+ }
+ }
+ }
+ },
+ "details": {
+ "$ref": "#/definitions/named_list/properties/items"
+ },
+ "tracking": {
+ "description": "Describes how this vulnerability should be tracked as the project changes.",
+ "oneOf": [
+ {
+ "description": "Declares that a series of items should be tracked using source-specific tracking methods.",
+ "required": [
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "source"
+ },
+ "items": {
+ "type": "array",
+ "items": {
+ "description": "An item that should be tracked using source-specific tracking methods.",
+ "type": "object",
+ "required": [
+ "signatures"
+ ],
+ "properties": {
+ "file": {
+ "type": "string",
+ "description": "Path to the file where the vulnerability is located."
+ },
+ "start_line": {
+ "type": "number",
+ "description": "The first line of the file that includes the vulnerability."
+ },
+ "end_line": {
+ "type": "number",
+ "description": "The last line of the file that includes the vulnerability."
+ },
+ "signatures": {
+ "type": "array",
+ "description": "An array of calculated tracking signatures for this tracking item.",
+ "minItems": 1,
+ "items": {
+ "description": "A calculated tracking signature value and metadata.",
+ "required": [
+ "algorithm",
+ "value"
+ ],
+ "properties": {
+ "algorithm": {
+ "type": "string",
+ "description": "The algorithm used to generate the signature."
+ },
+ "value": {
+ "type": "string",
+ "description": "The result of this signature algorithm."
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "Each tracking type must declare its own type."
+ }
+ }
+ },
+ "flags": {
+ "description": "Flags that can be attached to vulnerabilities.",
+ "type": "array",
+ "items": {
+ "type": "object",
+ "description": "Informational flags identified and assigned to a vulnerability.",
+ "required": [
+ "type",
+ "origin",
+ "description"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Result of the scan.",
+ "enum": [
+ "flagged-as-likely-false-positive"
+ ]
+ },
+ "origin": {
+ "minLength": 1,
+ "description": "Tool that issued the flag.",
+ "type": "string"
+ },
+ "description": {
+ "minLength": 1,
+ "description": "What the flag is about.",
+ "type": "string"
+ }
+ }
+ }
+ },
+ "location": {
+ "type": "object",
+ "description": "Identifies the vulnerability's location.",
+ "required": [
+ "file",
+ "dependency"
+ ],
+ "properties": {
+ "file": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Path to the manifest or lock file where the dependency is declared (such as yarn.lock)."
+ },
+ "dependency": {
+ "type": "object",
+ "description": "Describes the dependency of a project where the vulnerability is located.",
+ "properties": {
+ "package": {
+ "type": "object",
+ "description": "Provides information on the package where the vulnerability is located.",
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Name of the package where the vulnerability is located."
+ }
+ }
+ },
+ "version": {
+ "type": "string",
+ "description": "Version of the vulnerable package."
+ },
+ "iid": {
+ "description": "ID that identifies the dependency in the scope of a dependency file.",
+ "type": "number"
+ },
+ "direct": {
+ "type": "boolean",
+ "description": "Tells whether this is a direct, top-level dependency of the scanned project."
+ },
+ "dependency_path": {
+ "type": "array",
+ "description": "Ancestors of the dependency, starting from a direct project dependency, and ending with an immediate parent of the dependency. The dependency itself is excluded from the path. Direct dependencies have no path.",
+ "items": {
+ "type": "object",
+ "required": [
+ "iid"
+ ],
+ "properties": {
+ "iid": {
+ "type": "number",
+ "description": "ID that is unique in the scope of a parent object, and specific to the resource type."
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "remediations": {
+ "type": "array",
+ "description": "An array of objects containing information on available remediations, along with patch diffs to apply.",
+ "items": {
+ "type": "object",
+ "required": [
+ "fixes",
+ "summary",
+ "diff"
+ ],
+ "properties": {
+ "fixes": {
+ "type": "array",
+ "description": "An array of strings that represent references to vulnerabilities fixed by this remediation.",
+ "items": {
+ "type": "object",
+ "required": [
+ "cve"
+ ],
+ "properties": {
+ "cve": {
+ "type": "string",
+ "description": "(Deprecated - use vulnerabilities[].id instead) A fingerprint string value that represents a concrete finding. This is used to determine whether two findings are same, which may not be 100% accurate. Note that this is NOT a CVE as described by https://cve.mitre.org/."
+ }
+ }
+ }
+ },
+ "summary": {
+ "type": "string",
+ "minLength": 1,
+ "description": "An overview of how the vulnerabilities were fixed."
+ },
+ "diff": {
+ "type": "string",
+ "minLength": 1,
+ "description": "A base64-encoded remediation code diff, compatible with git apply."
+ }
+ }
+ }
+ },
+ "dependency_files": {
+ "type": "array",
+ "description": "List of dependency files identified in the project.",
+ "items": {
+ "type": "object",
+ "required": [
+ "path",
+ "package_manager",
+ "dependencies"
+ ],
+ "properties": {
+ "path": {
+ "type": "string",
+ "minLength": 1
+ },
+ "package_manager": {
+ "type": "string",
+ "minLength": 1
+ },
+ "dependencies": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "description": "Describes the dependency of a project where the vulnerability is located.",
+ "properties": {
+ "package": {
+ "type": "object",
+ "description": "Provides information on the package where the vulnerability is located.",
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Name of the package where the vulnerability is located."
+ }
+ }
+ },
+ "version": {
+ "type": "string",
+ "description": "Version of the vulnerable package."
+ },
+ "iid": {
+ "description": "ID that identifies the dependency in the scope of a dependency file.",
+ "type": "number"
+ },
+ "direct": {
+ "type": "boolean",
+ "description": "Tells whether this is a direct, top-level dependency of the scanned project."
+ },
+ "dependency_path": {
+ "type": "array",
+ "description": "Ancestors of the dependency, starting from a direct project dependency, and ending with an immediate parent of the dependency. The dependency itself is excluded from the path. Direct dependencies have no path.",
+ "items": {
+ "type": "object",
+ "required": [
+ "iid"
+ ],
+ "properties": {
+ "iid": {
+ "type": "number",
+ "description": "ID that is unique in the scope of a parent object, and specific to the resource type."
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/lib/gitlab/ci/parsers/security/validators/schemas/14.1.3/sast-report-format.json b/lib/gitlab/ci/parsers/security/validators/schemas/14.1.3/sast-report-format.json
new file mode 100644
index 00000000000..40d4d9f5287
--- /dev/null
+++ b/lib/gitlab/ci/parsers/security/validators/schemas/14.1.3/sast-report-format.json
@@ -0,0 +1,869 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "title": "Report format for GitLab SAST",
+ "description": "This schema provides the report format for Static Application Security Testing analyzers (https://docs.gitlab.com/ee/user/application_security/sast).",
+ "definitions": {
+ "detail_type": {
+ "oneOf": [
+ {
+ "$ref": "#/definitions/named_list"
+ },
+ {
+ "$ref": "#/definitions/list"
+ },
+ {
+ "$ref": "#/definitions/table"
+ },
+ {
+ "$ref": "#/definitions/text"
+ },
+ {
+ "$ref": "#/definitions/url"
+ },
+ {
+ "$ref": "#/definitions/code"
+ },
+ {
+ "$ref": "#/definitions/value"
+ },
+ {
+ "$ref": "#/definitions/diff"
+ },
+ {
+ "$ref": "#/definitions/markdown"
+ },
+ {
+ "$ref": "#/definitions/commit"
+ },
+ {
+ "$ref": "#/definitions/file_location"
+ },
+ {
+ "$ref": "#/definitions/module_location"
+ }
+ ]
+ },
+ "text_value": {
+ "type": "string"
+ },
+ "named_field": {
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "$ref": "#/definitions/text_value",
+ "minLength": 1
+ },
+ "description": {
+ "$ref": "#/definitions/text_value"
+ }
+ }
+ },
+ "named_list": {
+ "type": "object",
+ "description": "An object with named and typed fields",
+ "required": [
+ "type",
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "named-list"
+ },
+ "items": {
+ "type": "object",
+ "patternProperties": {
+ "^.*$": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/named_field"
+ },
+ {
+ "$ref": "#/definitions/detail_type"
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "list": {
+ "type": "object",
+ "description": "A list of typed fields",
+ "required": [
+ "type",
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "list"
+ },
+ "items": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ }
+ }
+ },
+ "table": {
+ "type": "object",
+ "description": "A table of typed fields",
+ "required": [
+ "type",
+ "rows"
+ ],
+ "properties": {
+ "type": {
+ "const": "table"
+ },
+ "header": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ },
+ "rows": {
+ "type": "array",
+ "items": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ }
+ }
+ }
+ },
+ "text": {
+ "type": "object",
+ "description": "Raw text",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "text"
+ },
+ "value": {
+ "$ref": "#/definitions/text_value"
+ }
+ }
+ },
+ "url": {
+ "type": "object",
+ "description": "A single URL",
+ "required": [
+ "type",
+ "href"
+ ],
+ "properties": {
+ "type": {
+ "const": "url"
+ },
+ "text": {
+ "$ref": "#/definitions/text_value"
+ },
+ "href": {
+ "type": "string",
+ "minLength": 1,
+ "examples": [
+ "http://mysite.com"
+ ]
+ }
+ }
+ },
+ "code": {
+ "type": "object",
+ "description": "A codeblock",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "code"
+ },
+ "value": {
+ "type": "string"
+ },
+ "lang": {
+ "type": "string",
+ "description": "A programming language"
+ }
+ }
+ },
+ "value": {
+ "type": "object",
+ "description": "A field that can store a range of types of value",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "value"
+ },
+ "value": {
+ "type": [
+ "number",
+ "string",
+ "boolean"
+ ]
+ }
+ }
+ },
+ "diff": {
+ "type": "object",
+ "description": "A diff",
+ "required": [
+ "type",
+ "before",
+ "after"
+ ],
+ "properties": {
+ "type": {
+ "const": "diff"
+ },
+ "before": {
+ "type": "string"
+ },
+ "after": {
+ "type": "string"
+ }
+ }
+ },
+ "markdown": {
+ "type": "object",
+ "description": "GitLab flavoured markdown, see https://docs.gitlab.com/ee/user/markdown.html",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "markdown"
+ },
+ "value": {
+ "$ref": "#/definitions/text_value",
+ "examples": [
+ "Here is markdown `inline code` #1 [test](gitlab.com)\n\n![GitLab Logo](https://about.gitlab.com/images/press/logo/preview/gitlab-logo-white-preview.png)"
+ ]
+ }
+ }
+ },
+ "commit": {
+ "type": "object",
+ "description": "A commit/tag/branch within the GitLab project",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "commit"
+ },
+ "value": {
+ "type": "string",
+ "description": "The commit SHA",
+ "minLength": 1
+ }
+ }
+ },
+ "file_location": {
+ "type": "object",
+ "description": "A location within a file in the project",
+ "required": [
+ "type",
+ "file_name",
+ "line_start"
+ ],
+ "properties": {
+ "type": {
+ "const": "file-location"
+ },
+ "file_name": {
+ "type": "string",
+ "minLength": 1
+ },
+ "line_start": {
+ "type": "integer"
+ },
+ "line_end": {
+ "type": "integer"
+ }
+ }
+ },
+ "module_location": {
+ "type": "object",
+ "description": "A location within a binary module of the form module+relative_offset",
+ "required": [
+ "type",
+ "module_name",
+ "offset"
+ ],
+ "properties": {
+ "type": {
+ "const": "module-location"
+ },
+ "module_name": {
+ "type": "string",
+ "minLength": 1,
+ "examples": [
+ "compiled_binary"
+ ]
+ },
+ "offset": {
+ "type": "integer",
+ "examples": [
+ 100
+ ]
+ }
+ }
+ }
+ },
+ "self": {
+ "version": "14.1.3"
+ },
+ "required": [
+ "version",
+ "vulnerabilities"
+ ],
+ "additionalProperties": true,
+ "properties": {
+ "scan": {
+ "type": "object",
+ "required": [
+ "end_time",
+ "scanner",
+ "start_time",
+ "status",
+ "type"
+ ],
+ "properties": {
+ "end_time": {
+ "type": "string",
+ "description": "ISO8601 UTC value with format yyyy-mm-ddThh:mm:ss, representing when the scan finished.",
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}\\:\\d{2}\\:\\d{2}$",
+ "examples": [
+ "2020-01-28T03:26:02"
+ ]
+ },
+ "messages": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "description": "Communication intended for the initiator of a scan.",
+ "required": [
+ "level",
+ "value"
+ ],
+ "properties": {
+ "level": {
+ "type": "string",
+ "description": "Describes the severity of the communication. Use info to communicate normal scan behaviour; warn to communicate a potentially recoverable problem, or a partial error; fatal to communicate an issue that causes the scan to halt.",
+ "enum": [
+ "info",
+ "warn",
+ "fatal"
+ ],
+ "examples": [
+ "info"
+ ]
+ },
+ "value": {
+ "type": "string",
+ "description": "The message to communicate.",
+ "minLength": 1,
+ "examples": [
+ "Permission denied, scanning aborted"
+ ]
+ }
+ }
+ }
+ },
+ "analyzer": {
+ "type": "object",
+ "description": "Object defining the analyzer used to perform the scan. Analyzers typically delegate to an underlying scanner to run the scan.",
+ "required": [
+ "id",
+ "name",
+ "version",
+ "vendor"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Unique id that identifies the analyzer.",
+ "minLength": 1,
+ "examples": [
+ "gitlab-dast"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "description": "A human readable value that identifies the analyzer, not required to be unique.",
+ "minLength": 1,
+ "examples": [
+ "GitLab DAST"
+ ]
+ },
+ "url": {
+ "type": "string",
+ "format": "uri",
+ "pattern": "^https?://.+",
+ "description": "A link to more information about the analyzer.",
+ "examples": [
+ "https://docs.gitlab.com/ee/user/application_security/dast"
+ ]
+ },
+ "vendor": {
+ "description": "The vendor/maintainer of the analyzer.",
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The name of the vendor.",
+ "minLength": 1,
+ "examples": [
+ "GitLab"
+ ]
+ }
+ }
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the analyzer.",
+ "minLength": 1,
+ "examples": [
+ "1.0.2"
+ ]
+ }
+ }
+ },
+ "scanner": {
+ "type": "object",
+ "description": "Object defining the scanner used to perform the scan.",
+ "required": [
+ "id",
+ "name",
+ "version",
+ "vendor"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Unique id that identifies the scanner.",
+ "minLength": 1,
+ "examples": [
+ "my-sast-scanner"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "description": "A human readable value that identifies the scanner, not required to be unique.",
+ "minLength": 1,
+ "examples": [
+ "My SAST Scanner"
+ ]
+ },
+ "url": {
+ "type": "string",
+ "description": "A link to more information about the scanner.",
+ "examples": [
+ "https://scanner.url"
+ ]
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the scanner.",
+ "minLength": 1,
+ "examples": [
+ "1.0.2"
+ ]
+ },
+ "vendor": {
+ "description": "The vendor/maintainer of the scanner.",
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The name of the vendor.",
+ "minLength": 1,
+ "examples": [
+ "GitLab"
+ ]
+ }
+ }
+ }
+ }
+ },
+ "start_time": {
+ "type": "string",
+ "description": "ISO8601 UTC value with format yyyy-mm-ddThh:mm:ss, representing when the scan started.",
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}\\:\\d{2}\\:\\d{2}$",
+ "examples": [
+ "2020-02-14T16:01:59"
+ ]
+ },
+ "status": {
+ "type": "string",
+ "description": "Result of the scan.",
+ "enum": [
+ "success",
+ "failure"
+ ]
+ },
+ "type": {
+ "type": "string",
+ "description": "Type of the scan.",
+ "enum": [
+ "sast"
+ ]
+ }
+ }
+ },
+ "schema": {
+ "type": "string",
+ "description": "URI pointing to the validating security report schema.",
+ "format": "uri"
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the schema to which the JSON report conforms.",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ },
+ "vulnerabilities": {
+ "type": "array",
+ "description": "Array of vulnerability objects.",
+ "items": {
+ "type": "object",
+ "description": "Describes the vulnerability using GitLab Flavored Markdown",
+ "required": [
+ "category",
+ "cve",
+ "identifiers",
+ "location",
+ "scanner"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Unique identifier of the vulnerability. This is recommended to be a UUID.",
+ "examples": [
+ "642735a5-1425-428d-8d4e-3c854885a3c9"
+ ]
+ },
+ "category": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Describes where this vulnerability belongs (for example, SAST, Dependency Scanning, and so on)."
+ },
+ "name": {
+ "type": "string",
+ "description": "The name of the vulnerability. This must not include the finding's specific information."
+ },
+ "message": {
+ "type": "string",
+ "description": "A short text section that describes the vulnerability. This may include the finding's specific information."
+ },
+ "description": {
+ "type": "string",
+ "description": "A long text section describing the vulnerability more fully."
+ },
+ "cve": {
+ "type": "string",
+ "description": "(Deprecated - use vulnerabilities[].id instead) A fingerprint string value that represents a concrete finding. This is used to determine whether two findings are same, which may not be 100% accurate. Note that this is NOT a CVE as described by https://cve.mitre.org/."
+ },
+ "severity": {
+ "type": "string",
+ "description": "How much the vulnerability impacts the software. Possible values are Info, Unknown, Low, Medium, High, or Critical. Note that some analyzers may not report all these possible values.",
+ "enum": [
+ "Info",
+ "Unknown",
+ "Low",
+ "Medium",
+ "High",
+ "Critical"
+ ]
+ },
+ "confidence": {
+ "type": "string",
+ "description": "How reliable the vulnerability's assessment is. Possible values are Ignore, Unknown, Experimental, Low, Medium, High, and Confirmed. Note that some analyzers may not report all these possible values.",
+ "enum": [
+ "Ignore",
+ "Unknown",
+ "Experimental",
+ "Low",
+ "Medium",
+ "High",
+ "Confirmed"
+ ]
+ },
+ "solution": {
+ "type": "string",
+ "description": "Explanation of how to fix the vulnerability."
+ },
+ "scanner": {
+ "description": "Describes the scanner used to find this vulnerability.",
+ "type": "object",
+ "required": [
+ "id",
+ "name"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "minLength": 1,
+ "description": "The scanner's ID, as a snake_case string."
+ },
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Human-readable name of the scanner."
+ }
+ }
+ },
+ "identifiers": {
+ "type": "array",
+ "minItems": 1,
+ "description": "An ordered array of references that identify a vulnerability on internal or external databases. The first identifier is the Primary Identifier, which has special meaning.",
+ "items": {
+ "type": "object",
+ "required": [
+ "type",
+ "name",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "for example, cve, cwe, osvdb, usn, or an analyzer-dependent type such as gemnasium).",
+ "minLength": 1
+ },
+ "name": {
+ "type": "string",
+ "description": "Human-readable name of the identifier.",
+ "minLength": 1
+ },
+ "url": {
+ "type": "string",
+ "description": "URL of the identifier's documentation.",
+ "format": "uri"
+ },
+ "value": {
+ "type": "string",
+ "description": "Value of the identifier, for matching purpose.",
+ "minLength": 1
+ }
+ }
+ }
+ },
+ "links": {
+ "type": "array",
+ "description": "An array of references to external documentation or articles that describe the vulnerability.",
+ "items": {
+ "type": "object",
+ "required": [
+ "url"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Name of the vulnerability details link."
+ },
+ "url": {
+ "type": "string",
+ "description": "URL of the vulnerability details document.",
+ "format": "uri"
+ }
+ }
+ }
+ },
+ "details": {
+ "$ref": "#/definitions/named_list/properties/items"
+ },
+ "tracking": {
+ "description": "Describes how this vulnerability should be tracked as the project changes.",
+ "oneOf": [
+ {
+ "description": "Declares that a series of items should be tracked using source-specific tracking methods.",
+ "required": [
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "source"
+ },
+ "items": {
+ "type": "array",
+ "items": {
+ "description": "An item that should be tracked using source-specific tracking methods.",
+ "type": "object",
+ "required": [
+ "signatures"
+ ],
+ "properties": {
+ "file": {
+ "type": "string",
+ "description": "Path to the file where the vulnerability is located."
+ },
+ "start_line": {
+ "type": "number",
+ "description": "The first line of the file that includes the vulnerability."
+ },
+ "end_line": {
+ "type": "number",
+ "description": "The last line of the file that includes the vulnerability."
+ },
+ "signatures": {
+ "type": "array",
+ "description": "An array of calculated tracking signatures for this tracking item.",
+ "minItems": 1,
+ "items": {
+ "description": "A calculated tracking signature value and metadata.",
+ "required": [
+ "algorithm",
+ "value"
+ ],
+ "properties": {
+ "algorithm": {
+ "type": "string",
+ "description": "The algorithm used to generate the signature."
+ },
+ "value": {
+ "type": "string",
+ "description": "The result of this signature algorithm."
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "Each tracking type must declare its own type."
+ }
+ }
+ },
+ "flags": {
+ "description": "Flags that can be attached to vulnerabilities.",
+ "type": "array",
+ "items": {
+ "type": "object",
+ "description": "Informational flags identified and assigned to a vulnerability.",
+ "required": [
+ "type",
+ "origin",
+ "description"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Result of the scan.",
+ "enum": [
+ "flagged-as-likely-false-positive"
+ ]
+ },
+ "origin": {
+ "minLength": 1,
+ "description": "Tool that issued the flag.",
+ "type": "string"
+ },
+ "description": {
+ "minLength": 1,
+ "description": "What the flag is about.",
+ "type": "string"
+ }
+ }
+ }
+ },
+ "location": {
+ "type": "object",
+ "description": "Identifies the vulnerability's location.",
+ "properties": {
+ "file": {
+ "type": "string",
+ "description": "Path to the file where the vulnerability is located."
+ },
+ "start_line": {
+ "type": "number",
+ "description": "The first line of the code affected by the vulnerability."
+ },
+ "end_line": {
+ "type": "number",
+ "description": "The last line of the code affected by the vulnerability."
+ },
+ "class": {
+ "type": "string",
+ "description": "Provides the name of the class where the vulnerability is located."
+ },
+ "method": {
+ "type": "string",
+ "description": "Provides the name of the method where the vulnerability is located."
+ }
+ }
+ },
+ "raw_source_code_extract": {
+ "type": "string",
+ "description": "Provides an unsanitized excerpt of the affected source code."
+ }
+ }
+ }
+ },
+ "remediations": {
+ "type": "array",
+ "description": "An array of objects containing information on available remediations, along with patch diffs to apply.",
+ "items": {
+ "type": "object",
+ "required": [
+ "fixes",
+ "summary",
+ "diff"
+ ],
+ "properties": {
+ "fixes": {
+ "type": "array",
+ "description": "An array of strings that represent references to vulnerabilities fixed by this remediation.",
+ "items": {
+ "type": "object",
+ "required": [
+ "cve"
+ ],
+ "properties": {
+ "cve": {
+ "type": "string",
+ "description": "(Deprecated - use vulnerabilities[].id instead) A fingerprint string value that represents a concrete finding. This is used to determine whether two findings are same, which may not be 100% accurate. Note that this is NOT a CVE as described by https://cve.mitre.org/."
+ }
+ }
+ }
+ },
+ "summary": {
+ "type": "string",
+ "minLength": 1,
+ "description": "An overview of how the vulnerabilities were fixed."
+ },
+ "diff": {
+ "type": "string",
+ "minLength": 1,
+ "description": "A base64-encoded remediation code diff, compatible with git apply."
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/lib/gitlab/ci/parsers/security/validators/schemas/14.1.3/secret-detection-report-format.json b/lib/gitlab/ci/parsers/security/validators/schemas/14.1.3/secret-detection-report-format.json
new file mode 100644
index 00000000000..cfde126dd7b
--- /dev/null
+++ b/lib/gitlab/ci/parsers/security/validators/schemas/14.1.3/secret-detection-report-format.json
@@ -0,0 +1,892 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "title": "Report format for GitLab Secret Detection",
+ "description": "This schema provides the the report format for the Secret Detection analyzer (https://docs.gitlab.com/ee/user/application_security/secret_detection)",
+ "definitions": {
+ "detail_type": {
+ "oneOf": [
+ {
+ "$ref": "#/definitions/named_list"
+ },
+ {
+ "$ref": "#/definitions/list"
+ },
+ {
+ "$ref": "#/definitions/table"
+ },
+ {
+ "$ref": "#/definitions/text"
+ },
+ {
+ "$ref": "#/definitions/url"
+ },
+ {
+ "$ref": "#/definitions/code"
+ },
+ {
+ "$ref": "#/definitions/value"
+ },
+ {
+ "$ref": "#/definitions/diff"
+ },
+ {
+ "$ref": "#/definitions/markdown"
+ },
+ {
+ "$ref": "#/definitions/commit"
+ },
+ {
+ "$ref": "#/definitions/file_location"
+ },
+ {
+ "$ref": "#/definitions/module_location"
+ }
+ ]
+ },
+ "text_value": {
+ "type": "string"
+ },
+ "named_field": {
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "$ref": "#/definitions/text_value",
+ "minLength": 1
+ },
+ "description": {
+ "$ref": "#/definitions/text_value"
+ }
+ }
+ },
+ "named_list": {
+ "type": "object",
+ "description": "An object with named and typed fields",
+ "required": [
+ "type",
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "named-list"
+ },
+ "items": {
+ "type": "object",
+ "patternProperties": {
+ "^.*$": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/named_field"
+ },
+ {
+ "$ref": "#/definitions/detail_type"
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "list": {
+ "type": "object",
+ "description": "A list of typed fields",
+ "required": [
+ "type",
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "list"
+ },
+ "items": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ }
+ }
+ },
+ "table": {
+ "type": "object",
+ "description": "A table of typed fields",
+ "required": [
+ "type",
+ "rows"
+ ],
+ "properties": {
+ "type": {
+ "const": "table"
+ },
+ "header": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ },
+ "rows": {
+ "type": "array",
+ "items": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ }
+ }
+ }
+ },
+ "text": {
+ "type": "object",
+ "description": "Raw text",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "text"
+ },
+ "value": {
+ "$ref": "#/definitions/text_value"
+ }
+ }
+ },
+ "url": {
+ "type": "object",
+ "description": "A single URL",
+ "required": [
+ "type",
+ "href"
+ ],
+ "properties": {
+ "type": {
+ "const": "url"
+ },
+ "text": {
+ "$ref": "#/definitions/text_value"
+ },
+ "href": {
+ "type": "string",
+ "minLength": 1,
+ "examples": [
+ "http://mysite.com"
+ ]
+ }
+ }
+ },
+ "code": {
+ "type": "object",
+ "description": "A codeblock",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "code"
+ },
+ "value": {
+ "type": "string"
+ },
+ "lang": {
+ "type": "string",
+ "description": "A programming language"
+ }
+ }
+ },
+ "value": {
+ "type": "object",
+ "description": "A field that can store a range of types of value",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "value"
+ },
+ "value": {
+ "type": [
+ "number",
+ "string",
+ "boolean"
+ ]
+ }
+ }
+ },
+ "diff": {
+ "type": "object",
+ "description": "A diff",
+ "required": [
+ "type",
+ "before",
+ "after"
+ ],
+ "properties": {
+ "type": {
+ "const": "diff"
+ },
+ "before": {
+ "type": "string"
+ },
+ "after": {
+ "type": "string"
+ }
+ }
+ },
+ "markdown": {
+ "type": "object",
+ "description": "GitLab flavoured markdown, see https://docs.gitlab.com/ee/user/markdown.html",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "markdown"
+ },
+ "value": {
+ "$ref": "#/definitions/text_value",
+ "examples": [
+ "Here is markdown `inline code` #1 [test](gitlab.com)\n\n![GitLab Logo](https://about.gitlab.com/images/press/logo/preview/gitlab-logo-white-preview.png)"
+ ]
+ }
+ }
+ },
+ "commit": {
+ "type": "object",
+ "description": "A commit/tag/branch within the GitLab project",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "commit"
+ },
+ "value": {
+ "type": "string",
+ "description": "The commit SHA",
+ "minLength": 1
+ }
+ }
+ },
+ "file_location": {
+ "type": "object",
+ "description": "A location within a file in the project",
+ "required": [
+ "type",
+ "file_name",
+ "line_start"
+ ],
+ "properties": {
+ "type": {
+ "const": "file-location"
+ },
+ "file_name": {
+ "type": "string",
+ "minLength": 1
+ },
+ "line_start": {
+ "type": "integer"
+ },
+ "line_end": {
+ "type": "integer"
+ }
+ }
+ },
+ "module_location": {
+ "type": "object",
+ "description": "A location within a binary module of the form module+relative_offset",
+ "required": [
+ "type",
+ "module_name",
+ "offset"
+ ],
+ "properties": {
+ "type": {
+ "const": "module-location"
+ },
+ "module_name": {
+ "type": "string",
+ "minLength": 1,
+ "examples": [
+ "compiled_binary"
+ ]
+ },
+ "offset": {
+ "type": "integer",
+ "examples": [
+ 100
+ ]
+ }
+ }
+ }
+ },
+ "self": {
+ "version": "14.1.3"
+ },
+ "required": [
+ "version",
+ "vulnerabilities"
+ ],
+ "additionalProperties": true,
+ "properties": {
+ "scan": {
+ "type": "object",
+ "required": [
+ "end_time",
+ "scanner",
+ "start_time",
+ "status",
+ "type"
+ ],
+ "properties": {
+ "end_time": {
+ "type": "string",
+ "description": "ISO8601 UTC value with format yyyy-mm-ddThh:mm:ss, representing when the scan finished.",
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}\\:\\d{2}\\:\\d{2}$",
+ "examples": [
+ "2020-01-28T03:26:02"
+ ]
+ },
+ "messages": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "description": "Communication intended for the initiator of a scan.",
+ "required": [
+ "level",
+ "value"
+ ],
+ "properties": {
+ "level": {
+ "type": "string",
+ "description": "Describes the severity of the communication. Use info to communicate normal scan behaviour; warn to communicate a potentially recoverable problem, or a partial error; fatal to communicate an issue that causes the scan to halt.",
+ "enum": [
+ "info",
+ "warn",
+ "fatal"
+ ],
+ "examples": [
+ "info"
+ ]
+ },
+ "value": {
+ "type": "string",
+ "description": "The message to communicate.",
+ "minLength": 1,
+ "examples": [
+ "Permission denied, scanning aborted"
+ ]
+ }
+ }
+ }
+ },
+ "analyzer": {
+ "type": "object",
+ "description": "Object defining the analyzer used to perform the scan. Analyzers typically delegate to an underlying scanner to run the scan.",
+ "required": [
+ "id",
+ "name",
+ "version",
+ "vendor"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Unique id that identifies the analyzer.",
+ "minLength": 1,
+ "examples": [
+ "gitlab-dast"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "description": "A human readable value that identifies the analyzer, not required to be unique.",
+ "minLength": 1,
+ "examples": [
+ "GitLab DAST"
+ ]
+ },
+ "url": {
+ "type": "string",
+ "format": "uri",
+ "pattern": "^https?://.+",
+ "description": "A link to more information about the analyzer.",
+ "examples": [
+ "https://docs.gitlab.com/ee/user/application_security/dast"
+ ]
+ },
+ "vendor": {
+ "description": "The vendor/maintainer of the analyzer.",
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The name of the vendor.",
+ "minLength": 1,
+ "examples": [
+ "GitLab"
+ ]
+ }
+ }
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the analyzer.",
+ "minLength": 1,
+ "examples": [
+ "1.0.2"
+ ]
+ }
+ }
+ },
+ "scanner": {
+ "type": "object",
+ "description": "Object defining the scanner used to perform the scan.",
+ "required": [
+ "id",
+ "name",
+ "version",
+ "vendor"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Unique id that identifies the scanner.",
+ "minLength": 1,
+ "examples": [
+ "my-sast-scanner"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "description": "A human readable value that identifies the scanner, not required to be unique.",
+ "minLength": 1,
+ "examples": [
+ "My SAST Scanner"
+ ]
+ },
+ "url": {
+ "type": "string",
+ "description": "A link to more information about the scanner.",
+ "examples": [
+ "https://scanner.url"
+ ]
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the scanner.",
+ "minLength": 1,
+ "examples": [
+ "1.0.2"
+ ]
+ },
+ "vendor": {
+ "description": "The vendor/maintainer of the scanner.",
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The name of the vendor.",
+ "minLength": 1,
+ "examples": [
+ "GitLab"
+ ]
+ }
+ }
+ }
+ }
+ },
+ "start_time": {
+ "type": "string",
+ "description": "ISO8601 UTC value with format yyyy-mm-ddThh:mm:ss, representing when the scan started.",
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}\\:\\d{2}\\:\\d{2}$",
+ "examples": [
+ "2020-02-14T16:01:59"
+ ]
+ },
+ "status": {
+ "type": "string",
+ "description": "Result of the scan.",
+ "enum": [
+ "success",
+ "failure"
+ ]
+ },
+ "type": {
+ "type": "string",
+ "description": "Type of the scan.",
+ "enum": [
+ "secret_detection"
+ ]
+ }
+ }
+ },
+ "schema": {
+ "type": "string",
+ "description": "URI pointing to the validating security report schema.",
+ "format": "uri"
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the schema to which the JSON report conforms.",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ },
+ "vulnerabilities": {
+ "type": "array",
+ "description": "Array of vulnerability objects.",
+ "items": {
+ "type": "object",
+ "description": "Describes the vulnerability using GitLab Flavored Markdown",
+ "required": [
+ "category",
+ "cve",
+ "identifiers",
+ "location",
+ "scanner"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Unique identifier of the vulnerability. This is recommended to be a UUID.",
+ "examples": [
+ "642735a5-1425-428d-8d4e-3c854885a3c9"
+ ]
+ },
+ "category": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Describes where this vulnerability belongs (for example, SAST, Dependency Scanning, and so on)."
+ },
+ "name": {
+ "type": "string",
+ "description": "The name of the vulnerability. This must not include the finding's specific information."
+ },
+ "message": {
+ "type": "string",
+ "description": "A short text section that describes the vulnerability. This may include the finding's specific information."
+ },
+ "description": {
+ "type": "string",
+ "description": "A long text section describing the vulnerability more fully."
+ },
+ "cve": {
+ "type": "string",
+ "description": "(Deprecated - use vulnerabilities[].id instead) A fingerprint string value that represents a concrete finding. This is used to determine whether two findings are same, which may not be 100% accurate. Note that this is NOT a CVE as described by https://cve.mitre.org/."
+ },
+ "severity": {
+ "type": "string",
+ "description": "How much the vulnerability impacts the software. Possible values are Info, Unknown, Low, Medium, High, or Critical. Note that some analyzers may not report all these possible values.",
+ "enum": [
+ "Info",
+ "Unknown",
+ "Low",
+ "Medium",
+ "High",
+ "Critical"
+ ]
+ },
+ "confidence": {
+ "type": "string",
+ "description": "How reliable the vulnerability's assessment is. Possible values are Ignore, Unknown, Experimental, Low, Medium, High, and Confirmed. Note that some analyzers may not report all these possible values.",
+ "enum": [
+ "Ignore",
+ "Unknown",
+ "Experimental",
+ "Low",
+ "Medium",
+ "High",
+ "Confirmed"
+ ]
+ },
+ "solution": {
+ "type": "string",
+ "description": "Explanation of how to fix the vulnerability."
+ },
+ "scanner": {
+ "description": "Describes the scanner used to find this vulnerability.",
+ "type": "object",
+ "required": [
+ "id",
+ "name"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "minLength": 1,
+ "description": "The scanner's ID, as a snake_case string."
+ },
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Human-readable name of the scanner."
+ }
+ }
+ },
+ "identifiers": {
+ "type": "array",
+ "minItems": 1,
+ "description": "An ordered array of references that identify a vulnerability on internal or external databases. The first identifier is the Primary Identifier, which has special meaning.",
+ "items": {
+ "type": "object",
+ "required": [
+ "type",
+ "name",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "for example, cve, cwe, osvdb, usn, or an analyzer-dependent type such as gemnasium).",
+ "minLength": 1
+ },
+ "name": {
+ "type": "string",
+ "description": "Human-readable name of the identifier.",
+ "minLength": 1
+ },
+ "url": {
+ "type": "string",
+ "description": "URL of the identifier's documentation.",
+ "format": "uri"
+ },
+ "value": {
+ "type": "string",
+ "description": "Value of the identifier, for matching purpose.",
+ "minLength": 1
+ }
+ }
+ }
+ },
+ "links": {
+ "type": "array",
+ "description": "An array of references to external documentation or articles that describe the vulnerability.",
+ "items": {
+ "type": "object",
+ "required": [
+ "url"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Name of the vulnerability details link."
+ },
+ "url": {
+ "type": "string",
+ "description": "URL of the vulnerability details document.",
+ "format": "uri"
+ }
+ }
+ }
+ },
+ "details": {
+ "$ref": "#/definitions/named_list/properties/items"
+ },
+ "tracking": {
+ "description": "Describes how this vulnerability should be tracked as the project changes.",
+ "oneOf": [
+ {
+ "description": "Declares that a series of items should be tracked using source-specific tracking methods.",
+ "required": [
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "source"
+ },
+ "items": {
+ "type": "array",
+ "items": {
+ "description": "An item that should be tracked using source-specific tracking methods.",
+ "type": "object",
+ "required": [
+ "signatures"
+ ],
+ "properties": {
+ "file": {
+ "type": "string",
+ "description": "Path to the file where the vulnerability is located."
+ },
+ "start_line": {
+ "type": "number",
+ "description": "The first line of the file that includes the vulnerability."
+ },
+ "end_line": {
+ "type": "number",
+ "description": "The last line of the file that includes the vulnerability."
+ },
+ "signatures": {
+ "type": "array",
+ "description": "An array of calculated tracking signatures for this tracking item.",
+ "minItems": 1,
+ "items": {
+ "description": "A calculated tracking signature value and metadata.",
+ "required": [
+ "algorithm",
+ "value"
+ ],
+ "properties": {
+ "algorithm": {
+ "type": "string",
+ "description": "The algorithm used to generate the signature."
+ },
+ "value": {
+ "type": "string",
+ "description": "The result of this signature algorithm."
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "Each tracking type must declare its own type."
+ }
+ }
+ },
+ "flags": {
+ "description": "Flags that can be attached to vulnerabilities.",
+ "type": "array",
+ "items": {
+ "type": "object",
+ "description": "Informational flags identified and assigned to a vulnerability.",
+ "required": [
+ "type",
+ "origin",
+ "description"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Result of the scan.",
+ "enum": [
+ "flagged-as-likely-false-positive"
+ ]
+ },
+ "origin": {
+ "minLength": 1,
+ "description": "Tool that issued the flag.",
+ "type": "string"
+ },
+ "description": {
+ "minLength": 1,
+ "description": "What the flag is about.",
+ "type": "string"
+ }
+ }
+ }
+ },
+ "location": {
+ "required": [
+ "commit"
+ ],
+ "properties": {
+ "file": {
+ "type": "string",
+ "description": "Path to the file where the vulnerability is located"
+ },
+ "commit": {
+ "type": "object",
+ "description": "Represents the commit in which the vulnerability was detected",
+ "required": [
+ "sha"
+ ],
+ "properties": {
+ "author": {
+ "type": "string"
+ },
+ "date": {
+ "type": "string"
+ },
+ "message": {
+ "type": "string"
+ },
+ "sha": {
+ "type": "string",
+ "minLength": 1
+ }
+ }
+ },
+ "start_line": {
+ "type": "number",
+ "description": "The first line of the code affected by the vulnerability"
+ },
+ "end_line": {
+ "type": "number",
+ "description": "The last line of the code affected by the vulnerability"
+ },
+ "class": {
+ "type": "string",
+ "description": "Provides the name of the class where the vulnerability is located"
+ },
+ "method": {
+ "type": "string",
+ "description": "Provides the name of the method where the vulnerability is located"
+ }
+ }
+ },
+ "raw_source_code_extract": {
+ "type": "string",
+ "description": "Provides an unsanitized excerpt of the affected source code."
+ }
+ }
+ }
+ },
+ "remediations": {
+ "type": "array",
+ "description": "An array of objects containing information on available remediations, along with patch diffs to apply.",
+ "items": {
+ "type": "object",
+ "required": [
+ "fixes",
+ "summary",
+ "diff"
+ ],
+ "properties": {
+ "fixes": {
+ "type": "array",
+ "description": "An array of strings that represent references to vulnerabilities fixed by this remediation.",
+ "items": {
+ "type": "object",
+ "required": [
+ "cve"
+ ],
+ "properties": {
+ "cve": {
+ "type": "string",
+ "description": "(Deprecated - use vulnerabilities[].id instead) A fingerprint string value that represents a concrete finding. This is used to determine whether two findings are same, which may not be 100% accurate. Note that this is NOT a CVE as described by https://cve.mitre.org/."
+ }
+ }
+ }
+ },
+ "summary": {
+ "type": "string",
+ "minLength": 1,
+ "description": "An overview of how the vulnerabilities were fixed."
+ },
+ "diff": {
+ "type": "string",
+ "minLength": 1,
+ "description": "A base64-encoded remediation code diff, compatible with git apply."
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/lib/gitlab/ci/parsers/security/validators/schemas/15.0.0/cluster-image-scanning-report-format.json b/lib/gitlab/ci/parsers/security/validators/schemas/15.0.0/cluster-image-scanning-report-format.json
new file mode 100644
index 00000000000..7ccb39a2b8e
--- /dev/null
+++ b/lib/gitlab/ci/parsers/security/validators/schemas/15.0.0/cluster-image-scanning-report-format.json
@@ -0,0 +1,946 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "https://gitlab.com/gitlab-org/security-products/security-report-schemas/-/raw/master/dist/cluster-image-scanning-report-format.json",
+ "title": "Report format for GitLab Cluster Image Scanning",
+ "description": "This schema provides the the report format for Cluster Image Scanning (https://docs.gitlab.com/ee/user/application_security/cluster_image_scanning/).",
+ "definitions": {
+ "detail_type": {
+ "oneOf": [
+ {
+ "$ref": "#/definitions/named_list"
+ },
+ {
+ "$ref": "#/definitions/list"
+ },
+ {
+ "$ref": "#/definitions/table"
+ },
+ {
+ "$ref": "#/definitions/text"
+ },
+ {
+ "$ref": "#/definitions/url"
+ },
+ {
+ "$ref": "#/definitions/code"
+ },
+ {
+ "$ref": "#/definitions/value"
+ },
+ {
+ "$ref": "#/definitions/diff"
+ },
+ {
+ "$ref": "#/definitions/markdown"
+ },
+ {
+ "$ref": "#/definitions/commit"
+ },
+ {
+ "$ref": "#/definitions/file_location"
+ },
+ {
+ "$ref": "#/definitions/module_location"
+ }
+ ]
+ },
+ "text_value": {
+ "type": "string"
+ },
+ "named_field": {
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "$ref": "#/definitions/text_value",
+ "minLength": 1
+ },
+ "description": {
+ "$ref": "#/definitions/text_value"
+ }
+ }
+ },
+ "named_list": {
+ "type": "object",
+ "description": "An object with named and typed fields",
+ "required": [
+ "type",
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "named-list"
+ },
+ "items": {
+ "type": "object",
+ "patternProperties": {
+ "^.*$": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/named_field"
+ },
+ {
+ "$ref": "#/definitions/detail_type"
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "list": {
+ "type": "object",
+ "description": "A list of typed fields",
+ "required": [
+ "type",
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "list"
+ },
+ "items": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ }
+ }
+ },
+ "table": {
+ "type": "object",
+ "description": "A table of typed fields",
+ "required": [
+ "type",
+ "rows"
+ ],
+ "properties": {
+ "type": {
+ "const": "table"
+ },
+ "header": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ },
+ "rows": {
+ "type": "array",
+ "items": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ }
+ }
+ }
+ },
+ "text": {
+ "type": "object",
+ "description": "Raw text",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "text"
+ },
+ "value": {
+ "$ref": "#/definitions/text_value"
+ }
+ }
+ },
+ "url": {
+ "type": "object",
+ "description": "A single URL",
+ "required": [
+ "type",
+ "href"
+ ],
+ "properties": {
+ "type": {
+ "const": "url"
+ },
+ "text": {
+ "$ref": "#/definitions/text_value"
+ },
+ "href": {
+ "type": "string",
+ "minLength": 1,
+ "examples": [
+ "http://mysite.com"
+ ]
+ }
+ }
+ },
+ "code": {
+ "type": "object",
+ "description": "A codeblock",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "code"
+ },
+ "value": {
+ "type": "string"
+ },
+ "lang": {
+ "type": "string",
+ "description": "A programming language"
+ }
+ }
+ },
+ "value": {
+ "type": "object",
+ "description": "A field that can store a range of types of value",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "value"
+ },
+ "value": {
+ "type": [
+ "number",
+ "string",
+ "boolean"
+ ]
+ }
+ }
+ },
+ "diff": {
+ "type": "object",
+ "description": "A diff",
+ "required": [
+ "type",
+ "before",
+ "after"
+ ],
+ "properties": {
+ "type": {
+ "const": "diff"
+ },
+ "before": {
+ "type": "string"
+ },
+ "after": {
+ "type": "string"
+ }
+ }
+ },
+ "markdown": {
+ "type": "object",
+ "description": "GitLab flavoured markdown, see https://docs.gitlab.com/ee/user/markdown.html",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "markdown"
+ },
+ "value": {
+ "$ref": "#/definitions/text_value",
+ "examples": [
+ "Here is markdown `inline code` #1 [test](gitlab.com)\n\n![GitLab Logo](https://about.gitlab.com/images/press/logo/preview/gitlab-logo-white-preview.png)"
+ ]
+ }
+ }
+ },
+ "commit": {
+ "type": "object",
+ "description": "A commit/tag/branch within the GitLab project",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "commit"
+ },
+ "value": {
+ "type": "string",
+ "description": "The commit SHA",
+ "minLength": 1
+ }
+ }
+ },
+ "file_location": {
+ "type": "object",
+ "description": "A location within a file in the project",
+ "required": [
+ "type",
+ "file_name",
+ "line_start"
+ ],
+ "properties": {
+ "type": {
+ "const": "file-location"
+ },
+ "file_name": {
+ "type": "string",
+ "minLength": 1
+ },
+ "line_start": {
+ "type": "integer"
+ },
+ "line_end": {
+ "type": "integer"
+ }
+ }
+ },
+ "module_location": {
+ "type": "object",
+ "description": "A location within a binary module of the form module+relative_offset",
+ "required": [
+ "type",
+ "module_name",
+ "offset"
+ ],
+ "properties": {
+ "type": {
+ "const": "module-location"
+ },
+ "module_name": {
+ "type": "string",
+ "minLength": 1,
+ "examples": [
+ "compiled_binary"
+ ]
+ },
+ "offset": {
+ "type": "integer",
+ "examples": [
+ 100
+ ]
+ }
+ }
+ }
+ },
+ "self": {
+ "version": "15.0.0"
+ },
+ "required": [
+ "scan",
+ "version",
+ "vulnerabilities"
+ ],
+ "additionalProperties": true,
+ "properties": {
+ "scan": {
+ "type": "object",
+ "required": [
+ "analyzer",
+ "end_time",
+ "scanner",
+ "start_time",
+ "status",
+ "type"
+ ],
+ "properties": {
+ "end_time": {
+ "type": "string",
+ "description": "ISO8601 UTC value with format yyyy-mm-ddThh:mm:ss, representing when the scan finished.",
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}$",
+ "examples": [
+ "2020-01-28T03:26:02"
+ ]
+ },
+ "messages": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "description": "Communication intended for the initiator of a scan.",
+ "required": [
+ "level",
+ "value"
+ ],
+ "properties": {
+ "level": {
+ "type": "string",
+ "description": "Describes the severity of the communication. Use info to communicate normal scan behaviour; warn to communicate a potentially recoverable problem, or a partial error; fatal to communicate an issue that causes the scan to halt.",
+ "enum": [
+ "info",
+ "warn",
+ "fatal"
+ ],
+ "examples": [
+ "info"
+ ]
+ },
+ "value": {
+ "type": "string",
+ "description": "The message to communicate.",
+ "minLength": 1,
+ "examples": [
+ "Permission denied, scanning aborted"
+ ]
+ }
+ }
+ }
+ },
+ "analyzer": {
+ "type": "object",
+ "description": "Object defining the analyzer used to perform the scan. Analyzers typically delegate to an underlying scanner to run the scan.",
+ "required": [
+ "id",
+ "name",
+ "version",
+ "vendor"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Unique id that identifies the analyzer.",
+ "minLength": 1,
+ "examples": [
+ "gitlab-dast"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "description": "A human readable value that identifies the analyzer, not required to be unique.",
+ "minLength": 1,
+ "examples": [
+ "GitLab DAST"
+ ]
+ },
+ "url": {
+ "type": "string",
+ "pattern": "^https?://.+",
+ "description": "A link to more information about the analyzer.",
+ "examples": [
+ "https://docs.gitlab.com/ee/user/application_security/dast"
+ ]
+ },
+ "vendor": {
+ "description": "The vendor/maintainer of the analyzer.",
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The name of the vendor.",
+ "minLength": 1,
+ "examples": [
+ "GitLab"
+ ]
+ }
+ }
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the analyzer.",
+ "minLength": 1,
+ "examples": [
+ "1.0.2"
+ ]
+ }
+ }
+ },
+ "scanner": {
+ "type": "object",
+ "description": "Object defining the scanner used to perform the scan.",
+ "required": [
+ "id",
+ "name",
+ "version",
+ "vendor"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Unique id that identifies the scanner.",
+ "minLength": 1,
+ "examples": [
+ "my-sast-scanner"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "description": "A human readable value that identifies the scanner, not required to be unique.",
+ "minLength": 1,
+ "examples": [
+ "My SAST Scanner"
+ ]
+ },
+ "url": {
+ "type": "string",
+ "description": "A link to more information about the scanner.",
+ "examples": [
+ "https://scanner.url"
+ ]
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the scanner.",
+ "minLength": 1,
+ "examples": [
+ "1.0.2"
+ ]
+ },
+ "vendor": {
+ "description": "The vendor/maintainer of the scanner.",
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The name of the vendor.",
+ "minLength": 1,
+ "examples": [
+ "GitLab"
+ ]
+ }
+ }
+ }
+ }
+ },
+ "start_time": {
+ "type": "string",
+ "description": "ISO8601 UTC value with format yyyy-mm-ddThh:mm:ss, representing when the scan started.",
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}$",
+ "examples": [
+ "2020-02-14T16:01:59"
+ ]
+ },
+ "status": {
+ "type": "string",
+ "description": "Result of the scan.",
+ "enum": [
+ "success",
+ "failure"
+ ]
+ },
+ "type": {
+ "type": "string",
+ "description": "Type of the scan.",
+ "enum": [
+ "cluster_image_scanning"
+ ]
+ }
+ }
+ },
+ "schema": {
+ "type": "string",
+ "description": "URI pointing to the validating security report schema.",
+ "pattern": "^https?://.+"
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the schema to which the JSON report conforms.",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ },
+ "vulnerabilities": {
+ "type": "array",
+ "description": "Array of vulnerability objects.",
+ "items": {
+ "type": "object",
+ "description": "Describes the vulnerability using GitLab Flavored Markdown",
+ "required": [
+ "id",
+ "identifiers",
+ "location"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Unique identifier of the vulnerability. This is recommended to be a UUID.",
+ "examples": [
+ "642735a5-1425-428d-8d4e-3c854885a3c9"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "maxLength": 255,
+ "description": "The name of the vulnerability. This must not include the finding's specific information."
+ },
+ "description": {
+ "type": "string",
+ "maxLength": 1048576,
+ "description": "A long text section describing the vulnerability more fully."
+ },
+ "severity": {
+ "type": "string",
+ "description": "How much the vulnerability impacts the software. Possible values are Info, Unknown, Low, Medium, High, or Critical. Note that some analyzers may not report all these possible values.",
+ "enum": [
+ "Info",
+ "Unknown",
+ "Low",
+ "Medium",
+ "High",
+ "Critical"
+ ]
+ },
+ "solution": {
+ "type": "string",
+ "maxLength": 7000,
+ "description": "Explanation of how to fix the vulnerability."
+ },
+ "identifiers": {
+ "type": "array",
+ "minItems": 1,
+ "description": "An ordered array of references that identify a vulnerability on internal or external databases. The first identifier is the Primary Identifier, which has special meaning.",
+ "items": {
+ "type": "object",
+ "required": [
+ "type",
+ "name",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "for example, cve, cwe, osvdb, usn, or an analyzer-dependent type such as gemnasium).",
+ "minLength": 1
+ },
+ "name": {
+ "type": "string",
+ "description": "Human-readable name of the identifier.",
+ "minLength": 1
+ },
+ "url": {
+ "type": "string",
+ "description": "URL of the identifier's documentation.",
+ "pattern": "^https?://.+"
+ },
+ "value": {
+ "type": "string",
+ "description": "Value of the identifier, for matching purpose.",
+ "minLength": 1
+ }
+ }
+ }
+ },
+ "links": {
+ "type": "array",
+ "description": "An array of references to external documentation or articles that describe the vulnerability.",
+ "items": {
+ "type": "object",
+ "required": [
+ "url"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Name of the vulnerability details link."
+ },
+ "url": {
+ "type": "string",
+ "description": "URL of the vulnerability details document.",
+ "pattern": "^https?://.+"
+ }
+ }
+ }
+ },
+ "details": {
+ "$ref": "#/definitions/named_list/properties/items"
+ },
+ "tracking": {
+ "description": "Describes how this vulnerability should be tracked as the project changes.",
+ "oneOf": [
+ {
+ "description": "Declares that a series of items should be tracked using source-specific tracking methods.",
+ "required": [
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "source"
+ },
+ "items": {
+ "type": "array",
+ "items": {
+ "description": "An item that should be tracked using source-specific tracking methods.",
+ "type": "object",
+ "required": [
+ "signatures"
+ ],
+ "properties": {
+ "file": {
+ "type": "string",
+ "description": "Path to the file where the vulnerability is located."
+ },
+ "start_line": {
+ "type": "number",
+ "description": "The first line of the file that includes the vulnerability."
+ },
+ "end_line": {
+ "type": "number",
+ "description": "The last line of the file that includes the vulnerability."
+ },
+ "signatures": {
+ "type": "array",
+ "description": "An array of calculated tracking signatures for this tracking item.",
+ "minItems": 1,
+ "items": {
+ "description": "A calculated tracking signature value and metadata.",
+ "required": [
+ "algorithm",
+ "value"
+ ],
+ "properties": {
+ "algorithm": {
+ "type": "string",
+ "description": "The algorithm used to generate the signature."
+ },
+ "value": {
+ "type": "string",
+ "description": "The result of this signature algorithm."
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "Each tracking type must declare its own type."
+ }
+ }
+ },
+ "flags": {
+ "description": "Flags that can be attached to vulnerabilities.",
+ "type": "array",
+ "items": {
+ "type": "object",
+ "description": "Informational flags identified and assigned to a vulnerability.",
+ "required": [
+ "type",
+ "origin",
+ "description"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Result of the scan.",
+ "enum": [
+ "flagged-as-likely-false-positive"
+ ]
+ },
+ "origin": {
+ "minLength": 1,
+ "description": "Tool that issued the flag.",
+ "type": "string"
+ },
+ "description": {
+ "minLength": 1,
+ "description": "What the flag is about.",
+ "type": "string"
+ }
+ }
+ }
+ },
+ "location": {
+ "type": "object",
+ "description": "Identifies the vulnerability's location.",
+ "required": [
+ "dependency",
+ "image",
+ "kubernetes_resource"
+ ],
+ "properties": {
+ "dependency": {
+ "type": "object",
+ "description": "Describes the dependency of a project where the vulnerability is located.",
+ "required": [
+ "package",
+ "version"
+ ],
+ "properties": {
+ "package": {
+ "type": "object",
+ "description": "Provides information on the package where the vulnerability is located.",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Name of the package where the vulnerability is located."
+ }
+ }
+ },
+ "version": {
+ "type": "string",
+ "description": "Version of the vulnerable package."
+ },
+ "iid": {
+ "description": "ID that identifies the dependency in the scope of a dependency file.",
+ "type": "number"
+ },
+ "direct": {
+ "type": "boolean",
+ "description": "Tells whether this is a direct, top-level dependency of the scanned project."
+ },
+ "dependency_path": {
+ "type": "array",
+ "description": "Ancestors of the dependency, starting from a direct project dependency, and ending with an immediate parent of the dependency. The dependency itself is excluded from the path. Direct dependencies have no path.",
+ "items": {
+ "type": "object",
+ "required": [
+ "iid"
+ ],
+ "properties": {
+ "iid": {
+ "type": "number",
+ "description": "ID that is unique in the scope of a parent object, and specific to the resource type."
+ }
+ }
+ }
+ }
+ }
+ },
+ "operating_system": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 255,
+ "description": "The operating system that contains the vulnerable package."
+ },
+ "image": {
+ "type": "string",
+ "minLength": 1,
+ "description": "The analyzed Docker image.",
+ "examples": [
+ "index.docker.io/library/nginx:1.21"
+ ]
+ },
+ "kubernetes_resource": {
+ "type": "object",
+ "description": "The specific Kubernetes resource that was scanned.",
+ "required": [
+ "namespace",
+ "kind",
+ "name",
+ "container_name"
+ ],
+ "properties": {
+ "namespace": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 255,
+ "description": "The Kubernetes namespace the resource that had its image scanned.",
+ "examples": [
+ "default",
+ "staging",
+ "production"
+ ]
+ },
+ "kind": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 255,
+ "description": "The Kubernetes kind the resource that had its image scanned.",
+ "examples": [
+ "Deployment",
+ "DaemonSet"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 255,
+ "description": "The name of the resource that had its image scanned.",
+ "examples": [
+ "nginx-ingress"
+ ]
+ },
+ "container_name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 255,
+ "description": "The name of the container that had its image scanned.",
+ "examples": [
+ "nginx"
+ ]
+ },
+ "agent_id": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 255,
+ "description": "The GitLab ID of the Kubernetes Agent which performed the scan.",
+ "examples": [
+ "1234"
+ ]
+ },
+ "cluster_id": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 255,
+ "description": "The GitLab ID of the Kubernetes cluster when using cluster integration.",
+ "examples": [
+ "1234"
+ ]
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "remediations": {
+ "type": "array",
+ "description": "An array of objects containing information on available remediations, along with patch diffs to apply.",
+ "items": {
+ "type": "object",
+ "required": [
+ "fixes",
+ "summary",
+ "diff"
+ ],
+ "properties": {
+ "fixes": {
+ "type": "array",
+ "description": "An array of strings that represent references to vulnerabilities fixed by this remediation.",
+ "items": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Unique identifier of the vulnerability. This is recommended to be a UUID.",
+ "examples": [
+ "642735a5-1425-428d-8d4e-3c854885a3c9"
+ ]
+ }
+ }
+ }
+ },
+ "summary": {
+ "type": "string",
+ "minLength": 1,
+ "description": "An overview of how the vulnerabilities were fixed."
+ },
+ "diff": {
+ "type": "string",
+ "minLength": 1,
+ "description": "A base64-encoded remediation code diff, compatible with git apply."
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/lib/gitlab/ci/parsers/security/validators/schemas/15.0.0/container-scanning-report-format.json b/lib/gitlab/ci/parsers/security/validators/schemas/15.0.0/container-scanning-report-format.json
new file mode 100644
index 00000000000..2517832853e
--- /dev/null
+++ b/lib/gitlab/ci/parsers/security/validators/schemas/15.0.0/container-scanning-report-format.json
@@ -0,0 +1,880 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "https://gitlab.com/gitlab-org/security-products/security-report-schemas/-/raw/master/dist/container-scanning-report-format.json",
+ "title": "Report format for GitLab Container Scanning",
+ "description": "This schema provides the the report format for Container Scanning (https://docs.gitlab.com/ee/user/application_security/container_scanning).",
+ "definitions": {
+ "detail_type": {
+ "oneOf": [
+ {
+ "$ref": "#/definitions/named_list"
+ },
+ {
+ "$ref": "#/definitions/list"
+ },
+ {
+ "$ref": "#/definitions/table"
+ },
+ {
+ "$ref": "#/definitions/text"
+ },
+ {
+ "$ref": "#/definitions/url"
+ },
+ {
+ "$ref": "#/definitions/code"
+ },
+ {
+ "$ref": "#/definitions/value"
+ },
+ {
+ "$ref": "#/definitions/diff"
+ },
+ {
+ "$ref": "#/definitions/markdown"
+ },
+ {
+ "$ref": "#/definitions/commit"
+ },
+ {
+ "$ref": "#/definitions/file_location"
+ },
+ {
+ "$ref": "#/definitions/module_location"
+ }
+ ]
+ },
+ "text_value": {
+ "type": "string"
+ },
+ "named_field": {
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "$ref": "#/definitions/text_value",
+ "minLength": 1
+ },
+ "description": {
+ "$ref": "#/definitions/text_value"
+ }
+ }
+ },
+ "named_list": {
+ "type": "object",
+ "description": "An object with named and typed fields",
+ "required": [
+ "type",
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "named-list"
+ },
+ "items": {
+ "type": "object",
+ "patternProperties": {
+ "^.*$": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/named_field"
+ },
+ {
+ "$ref": "#/definitions/detail_type"
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "list": {
+ "type": "object",
+ "description": "A list of typed fields",
+ "required": [
+ "type",
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "list"
+ },
+ "items": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ }
+ }
+ },
+ "table": {
+ "type": "object",
+ "description": "A table of typed fields",
+ "required": [
+ "type",
+ "rows"
+ ],
+ "properties": {
+ "type": {
+ "const": "table"
+ },
+ "header": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ },
+ "rows": {
+ "type": "array",
+ "items": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ }
+ }
+ }
+ },
+ "text": {
+ "type": "object",
+ "description": "Raw text",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "text"
+ },
+ "value": {
+ "$ref": "#/definitions/text_value"
+ }
+ }
+ },
+ "url": {
+ "type": "object",
+ "description": "A single URL",
+ "required": [
+ "type",
+ "href"
+ ],
+ "properties": {
+ "type": {
+ "const": "url"
+ },
+ "text": {
+ "$ref": "#/definitions/text_value"
+ },
+ "href": {
+ "type": "string",
+ "minLength": 1,
+ "examples": [
+ "http://mysite.com"
+ ]
+ }
+ }
+ },
+ "code": {
+ "type": "object",
+ "description": "A codeblock",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "code"
+ },
+ "value": {
+ "type": "string"
+ },
+ "lang": {
+ "type": "string",
+ "description": "A programming language"
+ }
+ }
+ },
+ "value": {
+ "type": "object",
+ "description": "A field that can store a range of types of value",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "value"
+ },
+ "value": {
+ "type": [
+ "number",
+ "string",
+ "boolean"
+ ]
+ }
+ }
+ },
+ "diff": {
+ "type": "object",
+ "description": "A diff",
+ "required": [
+ "type",
+ "before",
+ "after"
+ ],
+ "properties": {
+ "type": {
+ "const": "diff"
+ },
+ "before": {
+ "type": "string"
+ },
+ "after": {
+ "type": "string"
+ }
+ }
+ },
+ "markdown": {
+ "type": "object",
+ "description": "GitLab flavoured markdown, see https://docs.gitlab.com/ee/user/markdown.html",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "markdown"
+ },
+ "value": {
+ "$ref": "#/definitions/text_value",
+ "examples": [
+ "Here is markdown `inline code` #1 [test](gitlab.com)\n\n![GitLab Logo](https://about.gitlab.com/images/press/logo/preview/gitlab-logo-white-preview.png)"
+ ]
+ }
+ }
+ },
+ "commit": {
+ "type": "object",
+ "description": "A commit/tag/branch within the GitLab project",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "commit"
+ },
+ "value": {
+ "type": "string",
+ "description": "The commit SHA",
+ "minLength": 1
+ }
+ }
+ },
+ "file_location": {
+ "type": "object",
+ "description": "A location within a file in the project",
+ "required": [
+ "type",
+ "file_name",
+ "line_start"
+ ],
+ "properties": {
+ "type": {
+ "const": "file-location"
+ },
+ "file_name": {
+ "type": "string",
+ "minLength": 1
+ },
+ "line_start": {
+ "type": "integer"
+ },
+ "line_end": {
+ "type": "integer"
+ }
+ }
+ },
+ "module_location": {
+ "type": "object",
+ "description": "A location within a binary module of the form module+relative_offset",
+ "required": [
+ "type",
+ "module_name",
+ "offset"
+ ],
+ "properties": {
+ "type": {
+ "const": "module-location"
+ },
+ "module_name": {
+ "type": "string",
+ "minLength": 1,
+ "examples": [
+ "compiled_binary"
+ ]
+ },
+ "offset": {
+ "type": "integer",
+ "examples": [
+ 100
+ ]
+ }
+ }
+ }
+ },
+ "self": {
+ "version": "15.0.0"
+ },
+ "required": [
+ "scan",
+ "version",
+ "vulnerabilities"
+ ],
+ "additionalProperties": true,
+ "properties": {
+ "scan": {
+ "type": "object",
+ "required": [
+ "analyzer",
+ "end_time",
+ "scanner",
+ "start_time",
+ "status",
+ "type"
+ ],
+ "properties": {
+ "end_time": {
+ "type": "string",
+ "description": "ISO8601 UTC value with format yyyy-mm-ddThh:mm:ss, representing when the scan finished.",
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}$",
+ "examples": [
+ "2020-01-28T03:26:02"
+ ]
+ },
+ "messages": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "description": "Communication intended for the initiator of a scan.",
+ "required": [
+ "level",
+ "value"
+ ],
+ "properties": {
+ "level": {
+ "type": "string",
+ "description": "Describes the severity of the communication. Use info to communicate normal scan behaviour; warn to communicate a potentially recoverable problem, or a partial error; fatal to communicate an issue that causes the scan to halt.",
+ "enum": [
+ "info",
+ "warn",
+ "fatal"
+ ],
+ "examples": [
+ "info"
+ ]
+ },
+ "value": {
+ "type": "string",
+ "description": "The message to communicate.",
+ "minLength": 1,
+ "examples": [
+ "Permission denied, scanning aborted"
+ ]
+ }
+ }
+ }
+ },
+ "analyzer": {
+ "type": "object",
+ "description": "Object defining the analyzer used to perform the scan. Analyzers typically delegate to an underlying scanner to run the scan.",
+ "required": [
+ "id",
+ "name",
+ "version",
+ "vendor"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Unique id that identifies the analyzer.",
+ "minLength": 1,
+ "examples": [
+ "gitlab-dast"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "description": "A human readable value that identifies the analyzer, not required to be unique.",
+ "minLength": 1,
+ "examples": [
+ "GitLab DAST"
+ ]
+ },
+ "url": {
+ "type": "string",
+ "pattern": "^https?://.+",
+ "description": "A link to more information about the analyzer.",
+ "examples": [
+ "https://docs.gitlab.com/ee/user/application_security/dast"
+ ]
+ },
+ "vendor": {
+ "description": "The vendor/maintainer of the analyzer.",
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The name of the vendor.",
+ "minLength": 1,
+ "examples": [
+ "GitLab"
+ ]
+ }
+ }
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the analyzer.",
+ "minLength": 1,
+ "examples": [
+ "1.0.2"
+ ]
+ }
+ }
+ },
+ "scanner": {
+ "type": "object",
+ "description": "Object defining the scanner used to perform the scan.",
+ "required": [
+ "id",
+ "name",
+ "version",
+ "vendor"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Unique id that identifies the scanner.",
+ "minLength": 1,
+ "examples": [
+ "my-sast-scanner"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "description": "A human readable value that identifies the scanner, not required to be unique.",
+ "minLength": 1,
+ "examples": [
+ "My SAST Scanner"
+ ]
+ },
+ "url": {
+ "type": "string",
+ "description": "A link to more information about the scanner.",
+ "examples": [
+ "https://scanner.url"
+ ]
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the scanner.",
+ "minLength": 1,
+ "examples": [
+ "1.0.2"
+ ]
+ },
+ "vendor": {
+ "description": "The vendor/maintainer of the scanner.",
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The name of the vendor.",
+ "minLength": 1,
+ "examples": [
+ "GitLab"
+ ]
+ }
+ }
+ }
+ }
+ },
+ "start_time": {
+ "type": "string",
+ "description": "ISO8601 UTC value with format yyyy-mm-ddThh:mm:ss, representing when the scan started.",
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}$",
+ "examples": [
+ "2020-02-14T16:01:59"
+ ]
+ },
+ "status": {
+ "type": "string",
+ "description": "Result of the scan.",
+ "enum": [
+ "success",
+ "failure"
+ ]
+ },
+ "type": {
+ "type": "string",
+ "description": "Type of the scan.",
+ "enum": [
+ "container_scanning"
+ ]
+ }
+ }
+ },
+ "schema": {
+ "type": "string",
+ "description": "URI pointing to the validating security report schema.",
+ "pattern": "^https?://.+"
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the schema to which the JSON report conforms.",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ },
+ "vulnerabilities": {
+ "type": "array",
+ "description": "Array of vulnerability objects.",
+ "items": {
+ "type": "object",
+ "description": "Describes the vulnerability using GitLab Flavored Markdown",
+ "required": [
+ "id",
+ "identifiers",
+ "location"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Unique identifier of the vulnerability. This is recommended to be a UUID.",
+ "examples": [
+ "642735a5-1425-428d-8d4e-3c854885a3c9"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "maxLength": 255,
+ "description": "The name of the vulnerability. This must not include the finding's specific information."
+ },
+ "description": {
+ "type": "string",
+ "maxLength": 1048576,
+ "description": "A long text section describing the vulnerability more fully."
+ },
+ "severity": {
+ "type": "string",
+ "description": "How much the vulnerability impacts the software. Possible values are Info, Unknown, Low, Medium, High, or Critical. Note that some analyzers may not report all these possible values.",
+ "enum": [
+ "Info",
+ "Unknown",
+ "Low",
+ "Medium",
+ "High",
+ "Critical"
+ ]
+ },
+ "solution": {
+ "type": "string",
+ "maxLength": 7000,
+ "description": "Explanation of how to fix the vulnerability."
+ },
+ "identifiers": {
+ "type": "array",
+ "minItems": 1,
+ "description": "An ordered array of references that identify a vulnerability on internal or external databases. The first identifier is the Primary Identifier, which has special meaning.",
+ "items": {
+ "type": "object",
+ "required": [
+ "type",
+ "name",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "for example, cve, cwe, osvdb, usn, or an analyzer-dependent type such as gemnasium).",
+ "minLength": 1
+ },
+ "name": {
+ "type": "string",
+ "description": "Human-readable name of the identifier.",
+ "minLength": 1
+ },
+ "url": {
+ "type": "string",
+ "description": "URL of the identifier's documentation.",
+ "pattern": "^https?://.+"
+ },
+ "value": {
+ "type": "string",
+ "description": "Value of the identifier, for matching purpose.",
+ "minLength": 1
+ }
+ }
+ }
+ },
+ "links": {
+ "type": "array",
+ "description": "An array of references to external documentation or articles that describe the vulnerability.",
+ "items": {
+ "type": "object",
+ "required": [
+ "url"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Name of the vulnerability details link."
+ },
+ "url": {
+ "type": "string",
+ "description": "URL of the vulnerability details document.",
+ "pattern": "^https?://.+"
+ }
+ }
+ }
+ },
+ "details": {
+ "$ref": "#/definitions/named_list/properties/items"
+ },
+ "tracking": {
+ "description": "Describes how this vulnerability should be tracked as the project changes.",
+ "oneOf": [
+ {
+ "description": "Declares that a series of items should be tracked using source-specific tracking methods.",
+ "required": [
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "source"
+ },
+ "items": {
+ "type": "array",
+ "items": {
+ "description": "An item that should be tracked using source-specific tracking methods.",
+ "type": "object",
+ "required": [
+ "signatures"
+ ],
+ "properties": {
+ "file": {
+ "type": "string",
+ "description": "Path to the file where the vulnerability is located."
+ },
+ "start_line": {
+ "type": "number",
+ "description": "The first line of the file that includes the vulnerability."
+ },
+ "end_line": {
+ "type": "number",
+ "description": "The last line of the file that includes the vulnerability."
+ },
+ "signatures": {
+ "type": "array",
+ "description": "An array of calculated tracking signatures for this tracking item.",
+ "minItems": 1,
+ "items": {
+ "description": "A calculated tracking signature value and metadata.",
+ "required": [
+ "algorithm",
+ "value"
+ ],
+ "properties": {
+ "algorithm": {
+ "type": "string",
+ "description": "The algorithm used to generate the signature."
+ },
+ "value": {
+ "type": "string",
+ "description": "The result of this signature algorithm."
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "Each tracking type must declare its own type."
+ }
+ }
+ },
+ "flags": {
+ "description": "Flags that can be attached to vulnerabilities.",
+ "type": "array",
+ "items": {
+ "type": "object",
+ "description": "Informational flags identified and assigned to a vulnerability.",
+ "required": [
+ "type",
+ "origin",
+ "description"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Result of the scan.",
+ "enum": [
+ "flagged-as-likely-false-positive"
+ ]
+ },
+ "origin": {
+ "minLength": 1,
+ "description": "Tool that issued the flag.",
+ "type": "string"
+ },
+ "description": {
+ "minLength": 1,
+ "description": "What the flag is about.",
+ "type": "string"
+ }
+ }
+ }
+ },
+ "location": {
+ "type": "object",
+ "description": "Identifies the vulnerability's location.",
+ "required": [
+ "dependency",
+ "operating_system",
+ "image"
+ ],
+ "properties": {
+ "dependency": {
+ "type": "object",
+ "description": "Describes the dependency of a project where the vulnerability is located.",
+ "required": [
+ "package",
+ "version"
+ ],
+ "properties": {
+ "package": {
+ "type": "object",
+ "description": "Provides information on the package where the vulnerability is located.",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Name of the package where the vulnerability is located."
+ }
+ }
+ },
+ "version": {
+ "type": "string",
+ "description": "Version of the vulnerable package."
+ },
+ "iid": {
+ "description": "ID that identifies the dependency in the scope of a dependency file.",
+ "type": "number"
+ },
+ "direct": {
+ "type": "boolean",
+ "description": "Tells whether this is a direct, top-level dependency of the scanned project."
+ },
+ "dependency_path": {
+ "type": "array",
+ "description": "Ancestors of the dependency, starting from a direct project dependency, and ending with an immediate parent of the dependency. The dependency itself is excluded from the path. Direct dependencies have no path.",
+ "items": {
+ "type": "object",
+ "required": [
+ "iid"
+ ],
+ "properties": {
+ "iid": {
+ "type": "number",
+ "description": "ID that is unique in the scope of a parent object, and specific to the resource type."
+ }
+ }
+ }
+ }
+ }
+ },
+ "operating_system": {
+ "type": "string",
+ "minLength": 1,
+ "description": "The operating system that contains the vulnerable package."
+ },
+ "image": {
+ "type": "string",
+ "minLength": 1,
+ "pattern": "^[^:]+(:\\d+[^:]*)?:[^:]+(:[^:]+)?$",
+ "description": "The analyzed Docker image."
+ },
+ "default_branch_image": {
+ "type": "string",
+ "maxLength": 255,
+ "pattern": "^[a-zA-Z0-9/_.-]+(:\\d+[a-zA-Z0-9/_.-]*)?:[a-zA-Z0-9_.-]+$",
+ "description": "The name of the image on the default branch."
+ }
+ }
+ }
+ }
+ }
+ },
+ "remediations": {
+ "type": "array",
+ "description": "An array of objects containing information on available remediations, along with patch diffs to apply.",
+ "items": {
+ "type": "object",
+ "required": [
+ "fixes",
+ "summary",
+ "diff"
+ ],
+ "properties": {
+ "fixes": {
+ "type": "array",
+ "description": "An array of strings that represent references to vulnerabilities fixed by this remediation.",
+ "items": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Unique identifier of the vulnerability. This is recommended to be a UUID.",
+ "examples": [
+ "642735a5-1425-428d-8d4e-3c854885a3c9"
+ ]
+ }
+ }
+ }
+ },
+ "summary": {
+ "type": "string",
+ "minLength": 1,
+ "description": "An overview of how the vulnerabilities were fixed."
+ },
+ "diff": {
+ "type": "string",
+ "minLength": 1,
+ "description": "A base64-encoded remediation code diff, compatible with git apply."
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/lib/gitlab/ci/parsers/security/validators/schemas/15.0.0/coverage-fuzzing-report-format.json b/lib/gitlab/ci/parsers/security/validators/schemas/15.0.0/coverage-fuzzing-report-format.json
new file mode 100644
index 00000000000..a2f9eb12992
--- /dev/null
+++ b/lib/gitlab/ci/parsers/security/validators/schemas/15.0.0/coverage-fuzzing-report-format.json
@@ -0,0 +1,836 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "https://gitlab.com/gitlab-org/security-products/security-report-schemas/-/raw/master/dist/coverage-fuzzing-report-format.json",
+ "title": "Report format for GitLab Fuzz Testing",
+ "description": "This schema provides the report format for Coverage Guided Fuzz Testing (https://docs.gitlab.com/ee/user/application_security/coverage_fuzzing).",
+ "definitions": {
+ "detail_type": {
+ "oneOf": [
+ {
+ "$ref": "#/definitions/named_list"
+ },
+ {
+ "$ref": "#/definitions/list"
+ },
+ {
+ "$ref": "#/definitions/table"
+ },
+ {
+ "$ref": "#/definitions/text"
+ },
+ {
+ "$ref": "#/definitions/url"
+ },
+ {
+ "$ref": "#/definitions/code"
+ },
+ {
+ "$ref": "#/definitions/value"
+ },
+ {
+ "$ref": "#/definitions/diff"
+ },
+ {
+ "$ref": "#/definitions/markdown"
+ },
+ {
+ "$ref": "#/definitions/commit"
+ },
+ {
+ "$ref": "#/definitions/file_location"
+ },
+ {
+ "$ref": "#/definitions/module_location"
+ }
+ ]
+ },
+ "text_value": {
+ "type": "string"
+ },
+ "named_field": {
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "$ref": "#/definitions/text_value",
+ "minLength": 1
+ },
+ "description": {
+ "$ref": "#/definitions/text_value"
+ }
+ }
+ },
+ "named_list": {
+ "type": "object",
+ "description": "An object with named and typed fields",
+ "required": [
+ "type",
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "named-list"
+ },
+ "items": {
+ "type": "object",
+ "patternProperties": {
+ "^.*$": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/named_field"
+ },
+ {
+ "$ref": "#/definitions/detail_type"
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "list": {
+ "type": "object",
+ "description": "A list of typed fields",
+ "required": [
+ "type",
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "list"
+ },
+ "items": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ }
+ }
+ },
+ "table": {
+ "type": "object",
+ "description": "A table of typed fields",
+ "required": [
+ "type",
+ "rows"
+ ],
+ "properties": {
+ "type": {
+ "const": "table"
+ },
+ "header": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ },
+ "rows": {
+ "type": "array",
+ "items": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ }
+ }
+ }
+ },
+ "text": {
+ "type": "object",
+ "description": "Raw text",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "text"
+ },
+ "value": {
+ "$ref": "#/definitions/text_value"
+ }
+ }
+ },
+ "url": {
+ "type": "object",
+ "description": "A single URL",
+ "required": [
+ "type",
+ "href"
+ ],
+ "properties": {
+ "type": {
+ "const": "url"
+ },
+ "text": {
+ "$ref": "#/definitions/text_value"
+ },
+ "href": {
+ "type": "string",
+ "minLength": 1,
+ "examples": [
+ "http://mysite.com"
+ ]
+ }
+ }
+ },
+ "code": {
+ "type": "object",
+ "description": "A codeblock",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "code"
+ },
+ "value": {
+ "type": "string"
+ },
+ "lang": {
+ "type": "string",
+ "description": "A programming language"
+ }
+ }
+ },
+ "value": {
+ "type": "object",
+ "description": "A field that can store a range of types of value",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "value"
+ },
+ "value": {
+ "type": [
+ "number",
+ "string",
+ "boolean"
+ ]
+ }
+ }
+ },
+ "diff": {
+ "type": "object",
+ "description": "A diff",
+ "required": [
+ "type",
+ "before",
+ "after"
+ ],
+ "properties": {
+ "type": {
+ "const": "diff"
+ },
+ "before": {
+ "type": "string"
+ },
+ "after": {
+ "type": "string"
+ }
+ }
+ },
+ "markdown": {
+ "type": "object",
+ "description": "GitLab flavoured markdown, see https://docs.gitlab.com/ee/user/markdown.html",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "markdown"
+ },
+ "value": {
+ "$ref": "#/definitions/text_value",
+ "examples": [
+ "Here is markdown `inline code` #1 [test](gitlab.com)\n\n![GitLab Logo](https://about.gitlab.com/images/press/logo/preview/gitlab-logo-white-preview.png)"
+ ]
+ }
+ }
+ },
+ "commit": {
+ "type": "object",
+ "description": "A commit/tag/branch within the GitLab project",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "commit"
+ },
+ "value": {
+ "type": "string",
+ "description": "The commit SHA",
+ "minLength": 1
+ }
+ }
+ },
+ "file_location": {
+ "type": "object",
+ "description": "A location within a file in the project",
+ "required": [
+ "type",
+ "file_name",
+ "line_start"
+ ],
+ "properties": {
+ "type": {
+ "const": "file-location"
+ },
+ "file_name": {
+ "type": "string",
+ "minLength": 1
+ },
+ "line_start": {
+ "type": "integer"
+ },
+ "line_end": {
+ "type": "integer"
+ }
+ }
+ },
+ "module_location": {
+ "type": "object",
+ "description": "A location within a binary module of the form module+relative_offset",
+ "required": [
+ "type",
+ "module_name",
+ "offset"
+ ],
+ "properties": {
+ "type": {
+ "const": "module-location"
+ },
+ "module_name": {
+ "type": "string",
+ "minLength": 1,
+ "examples": [
+ "compiled_binary"
+ ]
+ },
+ "offset": {
+ "type": "integer",
+ "examples": [
+ 100
+ ]
+ }
+ }
+ }
+ },
+ "self": {
+ "version": "15.0.0"
+ },
+ "required": [
+ "scan",
+ "version",
+ "vulnerabilities"
+ ],
+ "additionalProperties": true,
+ "properties": {
+ "scan": {
+ "type": "object",
+ "required": [
+ "analyzer",
+ "end_time",
+ "scanner",
+ "start_time",
+ "status",
+ "type"
+ ],
+ "properties": {
+ "end_time": {
+ "type": "string",
+ "description": "ISO8601 UTC value with format yyyy-mm-ddThh:mm:ss, representing when the scan finished.",
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}$",
+ "examples": [
+ "2020-01-28T03:26:02"
+ ]
+ },
+ "messages": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "description": "Communication intended for the initiator of a scan.",
+ "required": [
+ "level",
+ "value"
+ ],
+ "properties": {
+ "level": {
+ "type": "string",
+ "description": "Describes the severity of the communication. Use info to communicate normal scan behaviour; warn to communicate a potentially recoverable problem, or a partial error; fatal to communicate an issue that causes the scan to halt.",
+ "enum": [
+ "info",
+ "warn",
+ "fatal"
+ ],
+ "examples": [
+ "info"
+ ]
+ },
+ "value": {
+ "type": "string",
+ "description": "The message to communicate.",
+ "minLength": 1,
+ "examples": [
+ "Permission denied, scanning aborted"
+ ]
+ }
+ }
+ }
+ },
+ "analyzer": {
+ "type": "object",
+ "description": "Object defining the analyzer used to perform the scan. Analyzers typically delegate to an underlying scanner to run the scan.",
+ "required": [
+ "id",
+ "name",
+ "version",
+ "vendor"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Unique id that identifies the analyzer.",
+ "minLength": 1,
+ "examples": [
+ "gitlab-dast"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "description": "A human readable value that identifies the analyzer, not required to be unique.",
+ "minLength": 1,
+ "examples": [
+ "GitLab DAST"
+ ]
+ },
+ "url": {
+ "type": "string",
+ "pattern": "^https?://.+",
+ "description": "A link to more information about the analyzer.",
+ "examples": [
+ "https://docs.gitlab.com/ee/user/application_security/dast"
+ ]
+ },
+ "vendor": {
+ "description": "The vendor/maintainer of the analyzer.",
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The name of the vendor.",
+ "minLength": 1,
+ "examples": [
+ "GitLab"
+ ]
+ }
+ }
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the analyzer.",
+ "minLength": 1,
+ "examples": [
+ "1.0.2"
+ ]
+ }
+ }
+ },
+ "scanner": {
+ "type": "object",
+ "description": "Object defining the scanner used to perform the scan.",
+ "required": [
+ "id",
+ "name",
+ "version",
+ "vendor"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Unique id that identifies the scanner.",
+ "minLength": 1,
+ "examples": [
+ "my-sast-scanner"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "description": "A human readable value that identifies the scanner, not required to be unique.",
+ "minLength": 1,
+ "examples": [
+ "My SAST Scanner"
+ ]
+ },
+ "url": {
+ "type": "string",
+ "description": "A link to more information about the scanner.",
+ "examples": [
+ "https://scanner.url"
+ ]
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the scanner.",
+ "minLength": 1,
+ "examples": [
+ "1.0.2"
+ ]
+ },
+ "vendor": {
+ "description": "The vendor/maintainer of the scanner.",
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The name of the vendor.",
+ "minLength": 1,
+ "examples": [
+ "GitLab"
+ ]
+ }
+ }
+ }
+ }
+ },
+ "start_time": {
+ "type": "string",
+ "description": "ISO8601 UTC value with format yyyy-mm-ddThh:mm:ss, representing when the scan started.",
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}$",
+ "examples": [
+ "2020-02-14T16:01:59"
+ ]
+ },
+ "status": {
+ "type": "string",
+ "description": "Result of the scan.",
+ "enum": [
+ "success",
+ "failure"
+ ]
+ },
+ "type": {
+ "type": "string",
+ "description": "Type of the scan.",
+ "enum": [
+ "coverage_fuzzing"
+ ]
+ }
+ }
+ },
+ "schema": {
+ "type": "string",
+ "description": "URI pointing to the validating security report schema.",
+ "pattern": "^https?://.+"
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the schema to which the JSON report conforms.",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ },
+ "vulnerabilities": {
+ "type": "array",
+ "description": "Array of vulnerability objects.",
+ "items": {
+ "type": "object",
+ "description": "Describes the vulnerability using GitLab Flavored Markdown",
+ "required": [
+ "id",
+ "identifiers",
+ "location"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Unique identifier of the vulnerability. This is recommended to be a UUID.",
+ "examples": [
+ "642735a5-1425-428d-8d4e-3c854885a3c9"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "maxLength": 255,
+ "description": "The name of the vulnerability. This must not include the finding's specific information."
+ },
+ "description": {
+ "type": "string",
+ "maxLength": 1048576,
+ "description": "A long text section describing the vulnerability more fully."
+ },
+ "severity": {
+ "type": "string",
+ "description": "How much the vulnerability impacts the software. Possible values are Info, Unknown, Low, Medium, High, or Critical. Note that some analyzers may not report all these possible values.",
+ "enum": [
+ "Info",
+ "Unknown",
+ "Low",
+ "Medium",
+ "High",
+ "Critical"
+ ]
+ },
+ "solution": {
+ "type": "string",
+ "maxLength": 7000,
+ "description": "Explanation of how to fix the vulnerability."
+ },
+ "identifiers": {
+ "type": "array",
+ "minItems": 1,
+ "description": "An ordered array of references that identify a vulnerability on internal or external databases. The first identifier is the Primary Identifier, which has special meaning.",
+ "items": {
+ "type": "object",
+ "required": [
+ "type",
+ "name",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "for example, cve, cwe, osvdb, usn, or an analyzer-dependent type such as gemnasium).",
+ "minLength": 1
+ },
+ "name": {
+ "type": "string",
+ "description": "Human-readable name of the identifier.",
+ "minLength": 1
+ },
+ "url": {
+ "type": "string",
+ "description": "URL of the identifier's documentation.",
+ "pattern": "^https?://.+"
+ },
+ "value": {
+ "type": "string",
+ "description": "Value of the identifier, for matching purpose.",
+ "minLength": 1
+ }
+ }
+ }
+ },
+ "links": {
+ "type": "array",
+ "description": "An array of references to external documentation or articles that describe the vulnerability.",
+ "items": {
+ "type": "object",
+ "required": [
+ "url"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Name of the vulnerability details link."
+ },
+ "url": {
+ "type": "string",
+ "description": "URL of the vulnerability details document.",
+ "pattern": "^https?://.+"
+ }
+ }
+ }
+ },
+ "details": {
+ "$ref": "#/definitions/named_list/properties/items"
+ },
+ "tracking": {
+ "description": "Describes how this vulnerability should be tracked as the project changes.",
+ "oneOf": [
+ {
+ "description": "Declares that a series of items should be tracked using source-specific tracking methods.",
+ "required": [
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "source"
+ },
+ "items": {
+ "type": "array",
+ "items": {
+ "description": "An item that should be tracked using source-specific tracking methods.",
+ "type": "object",
+ "required": [
+ "signatures"
+ ],
+ "properties": {
+ "file": {
+ "type": "string",
+ "description": "Path to the file where the vulnerability is located."
+ },
+ "start_line": {
+ "type": "number",
+ "description": "The first line of the file that includes the vulnerability."
+ },
+ "end_line": {
+ "type": "number",
+ "description": "The last line of the file that includes the vulnerability."
+ },
+ "signatures": {
+ "type": "array",
+ "description": "An array of calculated tracking signatures for this tracking item.",
+ "minItems": 1,
+ "items": {
+ "description": "A calculated tracking signature value and metadata.",
+ "required": [
+ "algorithm",
+ "value"
+ ],
+ "properties": {
+ "algorithm": {
+ "type": "string",
+ "description": "The algorithm used to generate the signature."
+ },
+ "value": {
+ "type": "string",
+ "description": "The result of this signature algorithm."
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "Each tracking type must declare its own type."
+ }
+ }
+ },
+ "flags": {
+ "description": "Flags that can be attached to vulnerabilities.",
+ "type": "array",
+ "items": {
+ "type": "object",
+ "description": "Informational flags identified and assigned to a vulnerability.",
+ "required": [
+ "type",
+ "origin",
+ "description"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Result of the scan.",
+ "enum": [
+ "flagged-as-likely-false-positive"
+ ]
+ },
+ "origin": {
+ "minLength": 1,
+ "description": "Tool that issued the flag.",
+ "type": "string"
+ },
+ "description": {
+ "minLength": 1,
+ "description": "What the flag is about.",
+ "type": "string"
+ }
+ }
+ }
+ },
+ "location": {
+ "description": "The location of the error",
+ "type": "object",
+ "properties": {
+ "crash_address": {
+ "type": "string",
+ "description": "The relative address in memory were the crash occurred.",
+ "examples": [
+ "0xabababab"
+ ]
+ },
+ "stacktrace_snippet": {
+ "type": "string",
+ "description": "The stack trace recorded during fuzzing resulting the crash.",
+ "examples": [
+ "func_a+0xabcd\nfunc_b+0xabcc"
+ ]
+ },
+ "crash_state": {
+ "type": "string",
+ "description": "Minimised and normalized crash stack-trace (called crash_state).",
+ "examples": [
+ "func_a+0xa\nfunc_b+0xb\nfunc_c+0xc"
+ ]
+ },
+ "crash_type": {
+ "type": "string",
+ "description": "Type of the crash.",
+ "examples": [
+ "Heap-Buffer-overflow",
+ "Division-by-zero"
+ ]
+ }
+ }
+ }
+ }
+ }
+ },
+ "remediations": {
+ "type": "array",
+ "description": "An array of objects containing information on available remediations, along with patch diffs to apply.",
+ "items": {
+ "type": "object",
+ "required": [
+ "fixes",
+ "summary",
+ "diff"
+ ],
+ "properties": {
+ "fixes": {
+ "type": "array",
+ "description": "An array of strings that represent references to vulnerabilities fixed by this remediation.",
+ "items": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Unique identifier of the vulnerability. This is recommended to be a UUID.",
+ "examples": [
+ "642735a5-1425-428d-8d4e-3c854885a3c9"
+ ]
+ }
+ }
+ }
+ },
+ "summary": {
+ "type": "string",
+ "minLength": 1,
+ "description": "An overview of how the vulnerabilities were fixed."
+ },
+ "diff": {
+ "type": "string",
+ "minLength": 1,
+ "description": "A base64-encoded remediation code diff, compatible with git apply."
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/lib/gitlab/ci/parsers/security/validators/schemas/15.0.0/dast-report-format.json b/lib/gitlab/ci/parsers/security/validators/schemas/15.0.0/dast-report-format.json
new file mode 100644
index 00000000000..10fafaf8975
--- /dev/null
+++ b/lib/gitlab/ci/parsers/security/validators/schemas/15.0.0/dast-report-format.json
@@ -0,0 +1,1241 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "https://gitlab.com/gitlab-org/security-products/security-report-schemas/-/raw/master/dist/dast-report-format.json",
+ "title": "Report format for GitLab DAST",
+ "description": "This schema provides the the report format for Dynamic Application Security Testing (https://docs.gitlab.com/ee/user/application_security/dast).",
+ "definitions": {
+ "detail_type": {
+ "oneOf": [
+ {
+ "$ref": "#/definitions/named_list"
+ },
+ {
+ "$ref": "#/definitions/list"
+ },
+ {
+ "$ref": "#/definitions/table"
+ },
+ {
+ "$ref": "#/definitions/text"
+ },
+ {
+ "$ref": "#/definitions/url"
+ },
+ {
+ "$ref": "#/definitions/code"
+ },
+ {
+ "$ref": "#/definitions/value"
+ },
+ {
+ "$ref": "#/definitions/diff"
+ },
+ {
+ "$ref": "#/definitions/markdown"
+ },
+ {
+ "$ref": "#/definitions/commit"
+ },
+ {
+ "$ref": "#/definitions/file_location"
+ },
+ {
+ "$ref": "#/definitions/module_location"
+ }
+ ]
+ },
+ "text_value": {
+ "type": "string"
+ },
+ "named_field": {
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "$ref": "#/definitions/text_value",
+ "minLength": 1
+ },
+ "description": {
+ "$ref": "#/definitions/text_value"
+ }
+ }
+ },
+ "named_list": {
+ "type": "object",
+ "description": "An object with named and typed fields",
+ "required": [
+ "type",
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "named-list"
+ },
+ "items": {
+ "type": "object",
+ "patternProperties": {
+ "^.*$": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/named_field"
+ },
+ {
+ "$ref": "#/definitions/detail_type"
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "list": {
+ "type": "object",
+ "description": "A list of typed fields",
+ "required": [
+ "type",
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "list"
+ },
+ "items": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ }
+ }
+ },
+ "table": {
+ "type": "object",
+ "description": "A table of typed fields",
+ "required": [
+ "type",
+ "rows"
+ ],
+ "properties": {
+ "type": {
+ "const": "table"
+ },
+ "header": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ },
+ "rows": {
+ "type": "array",
+ "items": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ }
+ }
+ }
+ },
+ "text": {
+ "type": "object",
+ "description": "Raw text",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "text"
+ },
+ "value": {
+ "$ref": "#/definitions/text_value"
+ }
+ }
+ },
+ "url": {
+ "type": "object",
+ "description": "A single URL",
+ "required": [
+ "type",
+ "href"
+ ],
+ "properties": {
+ "type": {
+ "const": "url"
+ },
+ "text": {
+ "$ref": "#/definitions/text_value"
+ },
+ "href": {
+ "type": "string",
+ "minLength": 1,
+ "examples": [
+ "http://mysite.com"
+ ]
+ }
+ }
+ },
+ "code": {
+ "type": "object",
+ "description": "A codeblock",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "code"
+ },
+ "value": {
+ "type": "string"
+ },
+ "lang": {
+ "type": "string",
+ "description": "A programming language"
+ }
+ }
+ },
+ "value": {
+ "type": "object",
+ "description": "A field that can store a range of types of value",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "value"
+ },
+ "value": {
+ "type": [
+ "number",
+ "string",
+ "boolean"
+ ]
+ }
+ }
+ },
+ "diff": {
+ "type": "object",
+ "description": "A diff",
+ "required": [
+ "type",
+ "before",
+ "after"
+ ],
+ "properties": {
+ "type": {
+ "const": "diff"
+ },
+ "before": {
+ "type": "string"
+ },
+ "after": {
+ "type": "string"
+ }
+ }
+ },
+ "markdown": {
+ "type": "object",
+ "description": "GitLab flavoured markdown, see https://docs.gitlab.com/ee/user/markdown.html",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "markdown"
+ },
+ "value": {
+ "$ref": "#/definitions/text_value",
+ "examples": [
+ "Here is markdown `inline code` #1 [test](gitlab.com)\n\n![GitLab Logo](https://about.gitlab.com/images/press/logo/preview/gitlab-logo-white-preview.png)"
+ ]
+ }
+ }
+ },
+ "commit": {
+ "type": "object",
+ "description": "A commit/tag/branch within the GitLab project",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "commit"
+ },
+ "value": {
+ "type": "string",
+ "description": "The commit SHA",
+ "minLength": 1
+ }
+ }
+ },
+ "file_location": {
+ "type": "object",
+ "description": "A location within a file in the project",
+ "required": [
+ "type",
+ "file_name",
+ "line_start"
+ ],
+ "properties": {
+ "type": {
+ "const": "file-location"
+ },
+ "file_name": {
+ "type": "string",
+ "minLength": 1
+ },
+ "line_start": {
+ "type": "integer"
+ },
+ "line_end": {
+ "type": "integer"
+ }
+ }
+ },
+ "module_location": {
+ "type": "object",
+ "description": "A location within a binary module of the form module+relative_offset",
+ "required": [
+ "type",
+ "module_name",
+ "offset"
+ ],
+ "properties": {
+ "type": {
+ "const": "module-location"
+ },
+ "module_name": {
+ "type": "string",
+ "minLength": 1,
+ "examples": [
+ "compiled_binary"
+ ]
+ },
+ "offset": {
+ "type": "integer",
+ "examples": [
+ 100
+ ]
+ }
+ }
+ }
+ },
+ "self": {
+ "version": "15.0.0"
+ },
+ "required": [
+ "scan",
+ "version",
+ "vulnerabilities"
+ ],
+ "additionalProperties": true,
+ "properties": {
+ "scan": {
+ "type": "object",
+ "required": [
+ "analyzer",
+ "end_time",
+ "scanned_resources",
+ "scanner",
+ "start_time",
+ "status",
+ "type"
+ ],
+ "properties": {
+ "end_time": {
+ "type": "string",
+ "description": "ISO8601 UTC value with format yyyy-mm-ddThh:mm:ss, representing when the scan finished.",
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}$",
+ "examples": [
+ "2020-01-28T03:26:02"
+ ]
+ },
+ "messages": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "description": "Communication intended for the initiator of a scan.",
+ "required": [
+ "level",
+ "value"
+ ],
+ "properties": {
+ "level": {
+ "type": "string",
+ "description": "Describes the severity of the communication. Use info to communicate normal scan behaviour; warn to communicate a potentially recoverable problem, or a partial error; fatal to communicate an issue that causes the scan to halt.",
+ "enum": [
+ "info",
+ "warn",
+ "fatal"
+ ],
+ "examples": [
+ "info"
+ ]
+ },
+ "value": {
+ "type": "string",
+ "description": "The message to communicate.",
+ "minLength": 1,
+ "examples": [
+ "Permission denied, scanning aborted"
+ ]
+ }
+ }
+ }
+ },
+ "analyzer": {
+ "type": "object",
+ "description": "Object defining the analyzer used to perform the scan. Analyzers typically delegate to an underlying scanner to run the scan.",
+ "required": [
+ "id",
+ "name",
+ "version",
+ "vendor"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Unique id that identifies the analyzer.",
+ "minLength": 1,
+ "examples": [
+ "gitlab-dast"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "description": "A human readable value that identifies the analyzer, not required to be unique.",
+ "minLength": 1,
+ "examples": [
+ "GitLab DAST"
+ ]
+ },
+ "url": {
+ "type": "string",
+ "pattern": "^https?://.+",
+ "description": "A link to more information about the analyzer.",
+ "examples": [
+ "https://docs.gitlab.com/ee/user/application_security/dast"
+ ]
+ },
+ "vendor": {
+ "description": "The vendor/maintainer of the analyzer.",
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The name of the vendor.",
+ "minLength": 1,
+ "examples": [
+ "GitLab"
+ ]
+ }
+ }
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the analyzer.",
+ "minLength": 1,
+ "examples": [
+ "1.0.2"
+ ]
+ }
+ }
+ },
+ "scanner": {
+ "type": "object",
+ "description": "Object defining the scanner used to perform the scan.",
+ "required": [
+ "id",
+ "name",
+ "version",
+ "vendor"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Unique id that identifies the scanner.",
+ "minLength": 1,
+ "examples": [
+ "my-sast-scanner"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "description": "A human readable value that identifies the scanner, not required to be unique.",
+ "minLength": 1,
+ "examples": [
+ "My SAST Scanner"
+ ]
+ },
+ "url": {
+ "type": "string",
+ "description": "A link to more information about the scanner.",
+ "examples": [
+ "https://scanner.url"
+ ]
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the scanner.",
+ "minLength": 1,
+ "examples": [
+ "1.0.2"
+ ]
+ },
+ "vendor": {
+ "description": "The vendor/maintainer of the scanner.",
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The name of the vendor.",
+ "minLength": 1,
+ "examples": [
+ "GitLab"
+ ]
+ }
+ }
+ }
+ }
+ },
+ "start_time": {
+ "type": "string",
+ "description": "ISO8601 UTC value with format yyyy-mm-ddThh:mm:ss, representing when the scan started.",
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}$",
+ "examples": [
+ "2020-02-14T16:01:59"
+ ]
+ },
+ "status": {
+ "type": "string",
+ "description": "Result of the scan.",
+ "enum": [
+ "success",
+ "failure"
+ ]
+ },
+ "type": {
+ "type": "string",
+ "description": "Type of the scan.",
+ "enum": [
+ "dast",
+ "api_fuzzing"
+ ]
+ },
+ "scanned_resources": {
+ "type": "array",
+ "description": "The attack surface scanned by DAST.",
+ "items": {
+ "type": "object",
+ "required": [
+ "method",
+ "url",
+ "type"
+ ],
+ "properties": {
+ "method": {
+ "type": "string",
+ "minLength": 1,
+ "description": "HTTP method of the scanned resource.",
+ "examples": [
+ "GET",
+ "POST",
+ "HEAD"
+ ]
+ },
+ "url": {
+ "type": "string",
+ "minLength": 1,
+ "description": "URL of the scanned resource.",
+ "examples": [
+ "http://my.site.com/a-page"
+ ]
+ },
+ "type": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Type of the scanned resource, for DAST, this must be 'url'.",
+ "examples": [
+ "url"
+ ]
+ }
+ }
+ }
+ }
+ }
+ },
+ "schema": {
+ "type": "string",
+ "description": "URI pointing to the validating security report schema.",
+ "pattern": "^https?://.+"
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the schema to which the JSON report conforms.",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ },
+ "vulnerabilities": {
+ "type": "array",
+ "description": "Array of vulnerability objects.",
+ "items": {
+ "type": "object",
+ "description": "Describes the vulnerability using GitLab Flavored Markdown",
+ "required": [
+ "id",
+ "identifiers",
+ "location"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Unique identifier of the vulnerability. This is recommended to be a UUID.",
+ "examples": [
+ "642735a5-1425-428d-8d4e-3c854885a3c9"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "maxLength": 255,
+ "description": "The name of the vulnerability. This must not include the finding's specific information."
+ },
+ "description": {
+ "type": "string",
+ "maxLength": 1048576,
+ "description": "A long text section describing the vulnerability more fully."
+ },
+ "severity": {
+ "type": "string",
+ "description": "How much the vulnerability impacts the software. Possible values are Info, Unknown, Low, Medium, High, or Critical. Note that some analyzers may not report all these possible values.",
+ "enum": [
+ "Info",
+ "Unknown",
+ "Low",
+ "Medium",
+ "High",
+ "Critical"
+ ]
+ },
+ "solution": {
+ "type": "string",
+ "maxLength": 7000,
+ "description": "Explanation of how to fix the vulnerability."
+ },
+ "identifiers": {
+ "type": "array",
+ "minItems": 1,
+ "description": "An ordered array of references that identify a vulnerability on internal or external databases. The first identifier is the Primary Identifier, which has special meaning.",
+ "items": {
+ "type": "object",
+ "required": [
+ "type",
+ "name",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "for example, cve, cwe, osvdb, usn, or an analyzer-dependent type such as gemnasium).",
+ "minLength": 1
+ },
+ "name": {
+ "type": "string",
+ "description": "Human-readable name of the identifier.",
+ "minLength": 1
+ },
+ "url": {
+ "type": "string",
+ "description": "URL of the identifier's documentation.",
+ "pattern": "^https?://.+"
+ },
+ "value": {
+ "type": "string",
+ "description": "Value of the identifier, for matching purpose.",
+ "minLength": 1
+ }
+ }
+ }
+ },
+ "links": {
+ "type": "array",
+ "description": "An array of references to external documentation or articles that describe the vulnerability.",
+ "items": {
+ "type": "object",
+ "required": [
+ "url"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Name of the vulnerability details link."
+ },
+ "url": {
+ "type": "string",
+ "description": "URL of the vulnerability details document.",
+ "pattern": "^https?://.+"
+ }
+ }
+ }
+ },
+ "details": {
+ "$ref": "#/definitions/named_list/properties/items"
+ },
+ "tracking": {
+ "description": "Describes how this vulnerability should be tracked as the project changes.",
+ "oneOf": [
+ {
+ "description": "Declares that a series of items should be tracked using source-specific tracking methods.",
+ "required": [
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "source"
+ },
+ "items": {
+ "type": "array",
+ "items": {
+ "description": "An item that should be tracked using source-specific tracking methods.",
+ "type": "object",
+ "required": [
+ "signatures"
+ ],
+ "properties": {
+ "file": {
+ "type": "string",
+ "description": "Path to the file where the vulnerability is located."
+ },
+ "start_line": {
+ "type": "number",
+ "description": "The first line of the file that includes the vulnerability."
+ },
+ "end_line": {
+ "type": "number",
+ "description": "The last line of the file that includes the vulnerability."
+ },
+ "signatures": {
+ "type": "array",
+ "description": "An array of calculated tracking signatures for this tracking item.",
+ "minItems": 1,
+ "items": {
+ "description": "A calculated tracking signature value and metadata.",
+ "required": [
+ "algorithm",
+ "value"
+ ],
+ "properties": {
+ "algorithm": {
+ "type": "string",
+ "description": "The algorithm used to generate the signature."
+ },
+ "value": {
+ "type": "string",
+ "description": "The result of this signature algorithm."
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "Each tracking type must declare its own type."
+ }
+ }
+ },
+ "flags": {
+ "description": "Flags that can be attached to vulnerabilities.",
+ "type": "array",
+ "items": {
+ "type": "object",
+ "description": "Informational flags identified and assigned to a vulnerability.",
+ "required": [
+ "type",
+ "origin",
+ "description"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Result of the scan.",
+ "enum": [
+ "flagged-as-likely-false-positive"
+ ]
+ },
+ "origin": {
+ "minLength": 1,
+ "description": "Tool that issued the flag.",
+ "type": "string"
+ },
+ "description": {
+ "minLength": 1,
+ "description": "What the flag is about.",
+ "type": "string"
+ }
+ }
+ }
+ },
+ "evidence": {
+ "type": "object",
+ "properties": {
+ "source": {
+ "type": "object",
+ "description": "Source of evidence",
+ "required": [
+ "id",
+ "name"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Unique source identifier",
+ "examples": [
+ "assert:LogAnalysis",
+ "assert:StatusCode"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Source display name",
+ "examples": [
+ "Log Analysis",
+ "Status Code"
+ ]
+ },
+ "url": {
+ "type": "string",
+ "description": "Link to additional information",
+ "examples": [
+ "https://docs.gitlab.com/ee/development/integrations/secure.html"
+ ]
+ }
+ }
+ },
+ "summary": {
+ "type": "string",
+ "description": "Human readable string containing evidence of the vulnerability.",
+ "examples": [
+ "Credit card 4111111111111111 found",
+ "Server leaked information nginx/1.17.6"
+ ]
+ },
+ "request": {
+ "type": "object",
+ "description": "An HTTP request.",
+ "required": [
+ "headers",
+ "method",
+ "url"
+ ],
+ "properties": {
+ "headers": {
+ "type": "array",
+ "description": "HTTP headers present on the request.",
+ "items": {
+ "type": "object",
+ "required": [
+ "name",
+ "value"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Name of the HTTP header.",
+ "examples": [
+ "Accept",
+ "Content-Length",
+ "Content-Type"
+ ]
+ },
+ "value": {
+ "type": "string",
+ "description": "Value of the HTTP header.",
+ "examples": [
+ "*/*",
+ "560",
+ "application/json; charset=utf-8"
+ ]
+ }
+ }
+ }
+ },
+ "method": {
+ "type": "string",
+ "minLength": 1,
+ "description": "HTTP method used in the request.",
+ "examples": [
+ "GET",
+ "POST"
+ ]
+ },
+ "url": {
+ "type": "string",
+ "minLength": 1,
+ "description": "URL of the request.",
+ "examples": [
+ "http://my.site.com/vulnerable-endpoint?show-credit-card"
+ ]
+ },
+ "body": {
+ "type": "string",
+ "description": "Body of the request for display purposes. Body must be suitable for display (not binary), and truncated to a reasonable size.",
+ "examples": [
+ "user=jsmith&first=%27&last=smith"
+ ]
+ }
+ }
+ },
+ "response": {
+ "type": "object",
+ "description": "An HTTP response.",
+ "required": [
+ "headers",
+ "reason_phrase",
+ "status_code"
+ ],
+ "properties": {
+ "headers": {
+ "type": "array",
+ "description": "HTTP headers present on the request.",
+ "items": {
+ "type": "object",
+ "required": [
+ "name",
+ "value"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Name of the HTTP header.",
+ "examples": [
+ "Accept",
+ "Content-Length",
+ "Content-Type"
+ ]
+ },
+ "value": {
+ "type": "string",
+ "description": "Value of the HTTP header.",
+ "examples": [
+ "*/*",
+ "560",
+ "application/json; charset=utf-8"
+ ]
+ }
+ }
+ }
+ },
+ "reason_phrase": {
+ "type": "string",
+ "description": "HTTP reason phrase of the response.",
+ "examples": [
+ "OK",
+ "Internal Server Error"
+ ]
+ },
+ "status_code": {
+ "type": "integer",
+ "description": "HTTP status code of the response.",
+ "examples": [
+ 200,
+ 500
+ ]
+ },
+ "body": {
+ "type": "string",
+ "description": "Body of the response for display purposes. Body must be suitable for display (not binary), and truncated to a reasonable size.",
+ "examples": [
+ "{\"user_id\": 2}"
+ ]
+ }
+ }
+ },
+ "supporting_messages": {
+ "type": "array",
+ "description": "Array of supporting http messages.",
+ "items": {
+ "type": "object",
+ "description": "A supporting http message.",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Message display name.",
+ "examples": [
+ "Unmodified",
+ "Recorded"
+ ]
+ },
+ "request": {
+ "type": "object",
+ "description": "An HTTP request.",
+ "required": [
+ "headers",
+ "method",
+ "url"
+ ],
+ "properties": {
+ "headers": {
+ "type": "array",
+ "description": "HTTP headers present on the request.",
+ "items": {
+ "type": "object",
+ "required": [
+ "name",
+ "value"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Name of the HTTP header.",
+ "examples": [
+ "Accept",
+ "Content-Length",
+ "Content-Type"
+ ]
+ },
+ "value": {
+ "type": "string",
+ "description": "Value of the HTTP header.",
+ "examples": [
+ "*/*",
+ "560",
+ "application/json; charset=utf-8"
+ ]
+ }
+ }
+ }
+ },
+ "method": {
+ "type": "string",
+ "minLength": 1,
+ "description": "HTTP method used in the request.",
+ "examples": [
+ "GET",
+ "POST"
+ ]
+ },
+ "url": {
+ "type": "string",
+ "minLength": 1,
+ "description": "URL of the request.",
+ "examples": [
+ "http://my.site.com/vulnerable-endpoint?show-credit-card"
+ ]
+ },
+ "body": {
+ "type": "string",
+ "description": "Body of the request for display purposes. Body must be suitable for display (not binary), and truncated to a reasonable size.",
+ "examples": [
+ "user=jsmith&first=%27&last=smith"
+ ]
+ }
+ }
+ },
+ "response": {
+ "type": "object",
+ "description": "An HTTP response.",
+ "required": [
+ "headers",
+ "reason_phrase",
+ "status_code"
+ ],
+ "properties": {
+ "headers": {
+ "type": "array",
+ "description": "HTTP headers present on the request.",
+ "items": {
+ "type": "object",
+ "required": [
+ "name",
+ "value"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Name of the HTTP header.",
+ "examples": [
+ "Accept",
+ "Content-Length",
+ "Content-Type"
+ ]
+ },
+ "value": {
+ "type": "string",
+ "description": "Value of the HTTP header.",
+ "examples": [
+ "*/*",
+ "560",
+ "application/json; charset=utf-8"
+ ]
+ }
+ }
+ }
+ },
+ "reason_phrase": {
+ "type": "string",
+ "description": "HTTP reason phrase of the response.",
+ "examples": [
+ "OK",
+ "Internal Server Error"
+ ]
+ },
+ "status_code": {
+ "type": "integer",
+ "description": "HTTP status code of the response.",
+ "examples": [
+ 200,
+ 500
+ ]
+ },
+ "body": {
+ "type": "string",
+ "description": "Body of the response for display purposes. Body must be suitable for display (not binary), and truncated to a reasonable size.",
+ "examples": [
+ "{\"user_id\": 2}"
+ ]
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "location": {
+ "type": "object",
+ "description": "Identifies the vulnerability's location.",
+ "properties": {
+ "hostname": {
+ "type": "string",
+ "description": "The protocol, domain, and port of the application where the vulnerability was found."
+ },
+ "method": {
+ "type": "string",
+ "description": "The HTTP method that was used to request the URL where the vulnerability was found."
+ },
+ "param": {
+ "type": "string",
+ "description": "A value provided by a vulnerability rule related to the found vulnerability. Examples include a header value, or a parameter used in a HTTP POST."
+ },
+ "path": {
+ "type": "string",
+ "description": "The path of the URL where the vulnerability was found. Typically, this would start with a forward slash."
+ }
+ }
+ },
+ "assets": {
+ "type": "array",
+ "description": "Array of build assets associated with vulnerability.",
+ "items": {
+ "type": "object",
+ "description": "Describes an asset associated with vulnerability.",
+ "required": [
+ "type",
+ "name",
+ "url"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "The type of asset",
+ "enum": [
+ "http_session",
+ "postman"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Display name for asset",
+ "examples": [
+ "HTTP Messages",
+ "Postman Collection"
+ ]
+ },
+ "url": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Link to asset in build artifacts",
+ "examples": [
+ "https://gitlab.com/gitlab-org/security-products/dast/-/jobs/626397001/artifacts/file//output/zap_session.data"
+ ]
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "remediations": {
+ "type": "array",
+ "description": "An array of objects containing information on available remediations, along with patch diffs to apply.",
+ "items": {
+ "type": "object",
+ "required": [
+ "fixes",
+ "summary",
+ "diff"
+ ],
+ "properties": {
+ "fixes": {
+ "type": "array",
+ "description": "An array of strings that represent references to vulnerabilities fixed by this remediation.",
+ "items": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Unique identifier of the vulnerability. This is recommended to be a UUID.",
+ "examples": [
+ "642735a5-1425-428d-8d4e-3c854885a3c9"
+ ]
+ }
+ }
+ }
+ },
+ "summary": {
+ "type": "string",
+ "minLength": 1,
+ "description": "An overview of how the vulnerabilities were fixed."
+ },
+ "diff": {
+ "type": "string",
+ "minLength": 1,
+ "description": "A base64-encoded remediation code diff, compatible with git apply."
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/lib/gitlab/ci/parsers/security/validators/schemas/15.0.0/dependency-scanning-report-format.json b/lib/gitlab/ci/parsers/security/validators/schemas/15.0.0/dependency-scanning-report-format.json
new file mode 100644
index 00000000000..ade1ce9ea8f
--- /dev/null
+++ b/lib/gitlab/ci/parsers/security/validators/schemas/15.0.0/dependency-scanning-report-format.json
@@ -0,0 +1,944 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "https://gitlab.com/gitlab-org/security-products/security-report-schemas/-/raw/master/dist/dependency-scanning-report-format.json",
+ "title": "Report format for GitLab Dependency Scanning",
+ "description": "This schema provides the the report format for Dependency Scanning analyzers (https://docs.gitlab.com/ee/user/application_security/dependency_scanning).",
+ "definitions": {
+ "detail_type": {
+ "oneOf": [
+ {
+ "$ref": "#/definitions/named_list"
+ },
+ {
+ "$ref": "#/definitions/list"
+ },
+ {
+ "$ref": "#/definitions/table"
+ },
+ {
+ "$ref": "#/definitions/text"
+ },
+ {
+ "$ref": "#/definitions/url"
+ },
+ {
+ "$ref": "#/definitions/code"
+ },
+ {
+ "$ref": "#/definitions/value"
+ },
+ {
+ "$ref": "#/definitions/diff"
+ },
+ {
+ "$ref": "#/definitions/markdown"
+ },
+ {
+ "$ref": "#/definitions/commit"
+ },
+ {
+ "$ref": "#/definitions/file_location"
+ },
+ {
+ "$ref": "#/definitions/module_location"
+ }
+ ]
+ },
+ "text_value": {
+ "type": "string"
+ },
+ "named_field": {
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "$ref": "#/definitions/text_value",
+ "minLength": 1
+ },
+ "description": {
+ "$ref": "#/definitions/text_value"
+ }
+ }
+ },
+ "named_list": {
+ "type": "object",
+ "description": "An object with named and typed fields",
+ "required": [
+ "type",
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "named-list"
+ },
+ "items": {
+ "type": "object",
+ "patternProperties": {
+ "^.*$": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/named_field"
+ },
+ {
+ "$ref": "#/definitions/detail_type"
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "list": {
+ "type": "object",
+ "description": "A list of typed fields",
+ "required": [
+ "type",
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "list"
+ },
+ "items": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ }
+ }
+ },
+ "table": {
+ "type": "object",
+ "description": "A table of typed fields",
+ "required": [
+ "type",
+ "rows"
+ ],
+ "properties": {
+ "type": {
+ "const": "table"
+ },
+ "header": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ },
+ "rows": {
+ "type": "array",
+ "items": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ }
+ }
+ }
+ },
+ "text": {
+ "type": "object",
+ "description": "Raw text",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "text"
+ },
+ "value": {
+ "$ref": "#/definitions/text_value"
+ }
+ }
+ },
+ "url": {
+ "type": "object",
+ "description": "A single URL",
+ "required": [
+ "type",
+ "href"
+ ],
+ "properties": {
+ "type": {
+ "const": "url"
+ },
+ "text": {
+ "$ref": "#/definitions/text_value"
+ },
+ "href": {
+ "type": "string",
+ "minLength": 1,
+ "examples": [
+ "http://mysite.com"
+ ]
+ }
+ }
+ },
+ "code": {
+ "type": "object",
+ "description": "A codeblock",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "code"
+ },
+ "value": {
+ "type": "string"
+ },
+ "lang": {
+ "type": "string",
+ "description": "A programming language"
+ }
+ }
+ },
+ "value": {
+ "type": "object",
+ "description": "A field that can store a range of types of value",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "value"
+ },
+ "value": {
+ "type": [
+ "number",
+ "string",
+ "boolean"
+ ]
+ }
+ }
+ },
+ "diff": {
+ "type": "object",
+ "description": "A diff",
+ "required": [
+ "type",
+ "before",
+ "after"
+ ],
+ "properties": {
+ "type": {
+ "const": "diff"
+ },
+ "before": {
+ "type": "string"
+ },
+ "after": {
+ "type": "string"
+ }
+ }
+ },
+ "markdown": {
+ "type": "object",
+ "description": "GitLab flavoured markdown, see https://docs.gitlab.com/ee/user/markdown.html",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "markdown"
+ },
+ "value": {
+ "$ref": "#/definitions/text_value",
+ "examples": [
+ "Here is markdown `inline code` #1 [test](gitlab.com)\n\n![GitLab Logo](https://about.gitlab.com/images/press/logo/preview/gitlab-logo-white-preview.png)"
+ ]
+ }
+ }
+ },
+ "commit": {
+ "type": "object",
+ "description": "A commit/tag/branch within the GitLab project",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "commit"
+ },
+ "value": {
+ "type": "string",
+ "description": "The commit SHA",
+ "minLength": 1
+ }
+ }
+ },
+ "file_location": {
+ "type": "object",
+ "description": "A location within a file in the project",
+ "required": [
+ "type",
+ "file_name",
+ "line_start"
+ ],
+ "properties": {
+ "type": {
+ "const": "file-location"
+ },
+ "file_name": {
+ "type": "string",
+ "minLength": 1
+ },
+ "line_start": {
+ "type": "integer"
+ },
+ "line_end": {
+ "type": "integer"
+ }
+ }
+ },
+ "module_location": {
+ "type": "object",
+ "description": "A location within a binary module of the form module+relative_offset",
+ "required": [
+ "type",
+ "module_name",
+ "offset"
+ ],
+ "properties": {
+ "type": {
+ "const": "module-location"
+ },
+ "module_name": {
+ "type": "string",
+ "minLength": 1,
+ "examples": [
+ "compiled_binary"
+ ]
+ },
+ "offset": {
+ "type": "integer",
+ "examples": [
+ 100
+ ]
+ }
+ }
+ }
+ },
+ "self": {
+ "version": "15.0.0"
+ },
+ "required": [
+ "dependency_files",
+ "scan",
+ "version",
+ "vulnerabilities"
+ ],
+ "additionalProperties": true,
+ "properties": {
+ "scan": {
+ "type": "object",
+ "required": [
+ "analyzer",
+ "end_time",
+ "scanner",
+ "start_time",
+ "status",
+ "type"
+ ],
+ "properties": {
+ "end_time": {
+ "type": "string",
+ "description": "ISO8601 UTC value with format yyyy-mm-ddThh:mm:ss, representing when the scan finished.",
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}$",
+ "examples": [
+ "2020-01-28T03:26:02"
+ ]
+ },
+ "messages": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "description": "Communication intended for the initiator of a scan.",
+ "required": [
+ "level",
+ "value"
+ ],
+ "properties": {
+ "level": {
+ "type": "string",
+ "description": "Describes the severity of the communication. Use info to communicate normal scan behaviour; warn to communicate a potentially recoverable problem, or a partial error; fatal to communicate an issue that causes the scan to halt.",
+ "enum": [
+ "info",
+ "warn",
+ "fatal"
+ ],
+ "examples": [
+ "info"
+ ]
+ },
+ "value": {
+ "type": "string",
+ "description": "The message to communicate.",
+ "minLength": 1,
+ "examples": [
+ "Permission denied, scanning aborted"
+ ]
+ }
+ }
+ }
+ },
+ "analyzer": {
+ "type": "object",
+ "description": "Object defining the analyzer used to perform the scan. Analyzers typically delegate to an underlying scanner to run the scan.",
+ "required": [
+ "id",
+ "name",
+ "version",
+ "vendor"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Unique id that identifies the analyzer.",
+ "minLength": 1,
+ "examples": [
+ "gitlab-dast"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "description": "A human readable value that identifies the analyzer, not required to be unique.",
+ "minLength": 1,
+ "examples": [
+ "GitLab DAST"
+ ]
+ },
+ "url": {
+ "type": "string",
+ "pattern": "^https?://.+",
+ "description": "A link to more information about the analyzer.",
+ "examples": [
+ "https://docs.gitlab.com/ee/user/application_security/dast"
+ ]
+ },
+ "vendor": {
+ "description": "The vendor/maintainer of the analyzer.",
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The name of the vendor.",
+ "minLength": 1,
+ "examples": [
+ "GitLab"
+ ]
+ }
+ }
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the analyzer.",
+ "minLength": 1,
+ "examples": [
+ "1.0.2"
+ ]
+ }
+ }
+ },
+ "scanner": {
+ "type": "object",
+ "description": "Object defining the scanner used to perform the scan.",
+ "required": [
+ "id",
+ "name",
+ "version",
+ "vendor"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Unique id that identifies the scanner.",
+ "minLength": 1,
+ "examples": [
+ "my-sast-scanner"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "description": "A human readable value that identifies the scanner, not required to be unique.",
+ "minLength": 1,
+ "examples": [
+ "My SAST Scanner"
+ ]
+ },
+ "url": {
+ "type": "string",
+ "description": "A link to more information about the scanner.",
+ "examples": [
+ "https://scanner.url"
+ ]
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the scanner.",
+ "minLength": 1,
+ "examples": [
+ "1.0.2"
+ ]
+ },
+ "vendor": {
+ "description": "The vendor/maintainer of the scanner.",
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The name of the vendor.",
+ "minLength": 1,
+ "examples": [
+ "GitLab"
+ ]
+ }
+ }
+ }
+ }
+ },
+ "start_time": {
+ "type": "string",
+ "description": "ISO8601 UTC value with format yyyy-mm-ddThh:mm:ss, representing when the scan started.",
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}$",
+ "examples": [
+ "2020-02-14T16:01:59"
+ ]
+ },
+ "status": {
+ "type": "string",
+ "description": "Result of the scan.",
+ "enum": [
+ "success",
+ "failure"
+ ]
+ },
+ "type": {
+ "type": "string",
+ "description": "Type of the scan.",
+ "enum": [
+ "dependency_scanning"
+ ]
+ }
+ }
+ },
+ "schema": {
+ "type": "string",
+ "description": "URI pointing to the validating security report schema.",
+ "pattern": "^https?://.+"
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the schema to which the JSON report conforms.",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ },
+ "vulnerabilities": {
+ "type": "array",
+ "description": "Array of vulnerability objects.",
+ "items": {
+ "type": "object",
+ "description": "Describes the vulnerability using GitLab Flavored Markdown",
+ "required": [
+ "id",
+ "identifiers",
+ "location"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Unique identifier of the vulnerability. This is recommended to be a UUID.",
+ "examples": [
+ "642735a5-1425-428d-8d4e-3c854885a3c9"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "maxLength": 255,
+ "description": "The name of the vulnerability. This must not include the finding's specific information."
+ },
+ "description": {
+ "type": "string",
+ "maxLength": 1048576,
+ "description": "A long text section describing the vulnerability more fully."
+ },
+ "severity": {
+ "type": "string",
+ "description": "How much the vulnerability impacts the software. Possible values are Info, Unknown, Low, Medium, High, or Critical. Note that some analyzers may not report all these possible values.",
+ "enum": [
+ "Info",
+ "Unknown",
+ "Low",
+ "Medium",
+ "High",
+ "Critical"
+ ]
+ },
+ "solution": {
+ "type": "string",
+ "maxLength": 7000,
+ "description": "Explanation of how to fix the vulnerability."
+ },
+ "identifiers": {
+ "type": "array",
+ "minItems": 1,
+ "description": "An ordered array of references that identify a vulnerability on internal or external databases. The first identifier is the Primary Identifier, which has special meaning.",
+ "items": {
+ "type": "object",
+ "required": [
+ "type",
+ "name",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "for example, cve, cwe, osvdb, usn, or an analyzer-dependent type such as gemnasium).",
+ "minLength": 1
+ },
+ "name": {
+ "type": "string",
+ "description": "Human-readable name of the identifier.",
+ "minLength": 1
+ },
+ "url": {
+ "type": "string",
+ "description": "URL of the identifier's documentation.",
+ "pattern": "^https?://.+"
+ },
+ "value": {
+ "type": "string",
+ "description": "Value of the identifier, for matching purpose.",
+ "minLength": 1
+ }
+ }
+ }
+ },
+ "links": {
+ "type": "array",
+ "description": "An array of references to external documentation or articles that describe the vulnerability.",
+ "items": {
+ "type": "object",
+ "required": [
+ "url"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Name of the vulnerability details link."
+ },
+ "url": {
+ "type": "string",
+ "description": "URL of the vulnerability details document.",
+ "pattern": "^https?://.+"
+ }
+ }
+ }
+ },
+ "details": {
+ "$ref": "#/definitions/named_list/properties/items"
+ },
+ "tracking": {
+ "description": "Describes how this vulnerability should be tracked as the project changes.",
+ "oneOf": [
+ {
+ "description": "Declares that a series of items should be tracked using source-specific tracking methods.",
+ "required": [
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "source"
+ },
+ "items": {
+ "type": "array",
+ "items": {
+ "description": "An item that should be tracked using source-specific tracking methods.",
+ "type": "object",
+ "required": [
+ "signatures"
+ ],
+ "properties": {
+ "file": {
+ "type": "string",
+ "description": "Path to the file where the vulnerability is located."
+ },
+ "start_line": {
+ "type": "number",
+ "description": "The first line of the file that includes the vulnerability."
+ },
+ "end_line": {
+ "type": "number",
+ "description": "The last line of the file that includes the vulnerability."
+ },
+ "signatures": {
+ "type": "array",
+ "description": "An array of calculated tracking signatures for this tracking item.",
+ "minItems": 1,
+ "items": {
+ "description": "A calculated tracking signature value and metadata.",
+ "required": [
+ "algorithm",
+ "value"
+ ],
+ "properties": {
+ "algorithm": {
+ "type": "string",
+ "description": "The algorithm used to generate the signature."
+ },
+ "value": {
+ "type": "string",
+ "description": "The result of this signature algorithm."
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "Each tracking type must declare its own type."
+ }
+ }
+ },
+ "flags": {
+ "description": "Flags that can be attached to vulnerabilities.",
+ "type": "array",
+ "items": {
+ "type": "object",
+ "description": "Informational flags identified and assigned to a vulnerability.",
+ "required": [
+ "type",
+ "origin",
+ "description"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Result of the scan.",
+ "enum": [
+ "flagged-as-likely-false-positive"
+ ]
+ },
+ "origin": {
+ "minLength": 1,
+ "description": "Tool that issued the flag.",
+ "type": "string"
+ },
+ "description": {
+ "minLength": 1,
+ "description": "What the flag is about.",
+ "type": "string"
+ }
+ }
+ }
+ },
+ "location": {
+ "type": "object",
+ "description": "Identifies the vulnerability's location.",
+ "required": [
+ "file",
+ "dependency"
+ ],
+ "properties": {
+ "file": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Path to the manifest or lock file where the dependency is declared (such as yarn.lock)."
+ },
+ "dependency": {
+ "type": "object",
+ "description": "Describes the dependency of a project where the vulnerability is located.",
+ "required": [
+ "package",
+ "version"
+ ],
+ "properties": {
+ "package": {
+ "type": "object",
+ "description": "Provides information on the package where the vulnerability is located.",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Name of the package where the vulnerability is located."
+ }
+ }
+ },
+ "version": {
+ "type": "string",
+ "description": "Version of the vulnerable package."
+ },
+ "iid": {
+ "description": "ID that identifies the dependency in the scope of a dependency file.",
+ "type": "number"
+ },
+ "direct": {
+ "type": "boolean",
+ "description": "Tells whether this is a direct, top-level dependency of the scanned project."
+ },
+ "dependency_path": {
+ "type": "array",
+ "description": "Ancestors of the dependency, starting from a direct project dependency, and ending with an immediate parent of the dependency. The dependency itself is excluded from the path. Direct dependencies have no path.",
+ "items": {
+ "type": "object",
+ "required": [
+ "iid"
+ ],
+ "properties": {
+ "iid": {
+ "type": "number",
+ "description": "ID that is unique in the scope of a parent object, and specific to the resource type."
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "remediations": {
+ "type": "array",
+ "description": "An array of objects containing information on available remediations, along with patch diffs to apply.",
+ "items": {
+ "type": "object",
+ "required": [
+ "fixes",
+ "summary",
+ "diff"
+ ],
+ "properties": {
+ "fixes": {
+ "type": "array",
+ "description": "An array of strings that represent references to vulnerabilities fixed by this remediation.",
+ "items": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Unique identifier of the vulnerability. This is recommended to be a UUID.",
+ "examples": [
+ "642735a5-1425-428d-8d4e-3c854885a3c9"
+ ]
+ }
+ }
+ }
+ },
+ "summary": {
+ "type": "string",
+ "minLength": 1,
+ "description": "An overview of how the vulnerabilities were fixed."
+ },
+ "diff": {
+ "type": "string",
+ "minLength": 1,
+ "description": "A base64-encoded remediation code diff, compatible with git apply."
+ }
+ }
+ }
+ },
+ "dependency_files": {
+ "type": "array",
+ "description": "List of dependency files identified in the project.",
+ "items": {
+ "type": "object",
+ "required": [
+ "path",
+ "package_manager",
+ "dependencies"
+ ],
+ "properties": {
+ "path": {
+ "type": "string",
+ "minLength": 1
+ },
+ "package_manager": {
+ "type": "string",
+ "minLength": 1
+ },
+ "dependencies": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "description": "Describes the dependency of a project where the vulnerability is located.",
+ "required": [
+ "package",
+ "version"
+ ],
+ "properties": {
+ "package": {
+ "type": "object",
+ "description": "Provides information on the package where the vulnerability is located.",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Name of the package where the vulnerability is located."
+ }
+ }
+ },
+ "version": {
+ "type": "string",
+ "description": "Version of the vulnerable package."
+ },
+ "iid": {
+ "description": "ID that identifies the dependency in the scope of a dependency file.",
+ "type": "number"
+ },
+ "direct": {
+ "type": "boolean",
+ "description": "Tells whether this is a direct, top-level dependency of the scanned project."
+ },
+ "dependency_path": {
+ "type": "array",
+ "description": "Ancestors of the dependency, starting from a direct project dependency, and ending with an immediate parent of the dependency. The dependency itself is excluded from the path. Direct dependencies have no path.",
+ "items": {
+ "type": "object",
+ "required": [
+ "iid"
+ ],
+ "properties": {
+ "iid": {
+ "type": "number",
+ "description": "ID that is unique in the scope of a parent object, and specific to the resource type."
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/lib/gitlab/ci/parsers/security/validators/schemas/15.0.0/sast-report-format.json b/lib/gitlab/ci/parsers/security/validators/schemas/15.0.0/sast-report-format.json
new file mode 100644
index 00000000000..9fae45d728e
--- /dev/null
+++ b/lib/gitlab/ci/parsers/security/validators/schemas/15.0.0/sast-report-format.json
@@ -0,0 +1,831 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "https://gitlab.com/gitlab-org/security-products/security-report-schemas/-/raw/master/dist/sast-report-format.json",
+ "title": "Report format for GitLab SAST",
+ "description": "This schema provides the report format for Static Application Security Testing analyzers (https://docs.gitlab.com/ee/user/application_security/sast).",
+ "definitions": {
+ "detail_type": {
+ "oneOf": [
+ {
+ "$ref": "#/definitions/named_list"
+ },
+ {
+ "$ref": "#/definitions/list"
+ },
+ {
+ "$ref": "#/definitions/table"
+ },
+ {
+ "$ref": "#/definitions/text"
+ },
+ {
+ "$ref": "#/definitions/url"
+ },
+ {
+ "$ref": "#/definitions/code"
+ },
+ {
+ "$ref": "#/definitions/value"
+ },
+ {
+ "$ref": "#/definitions/diff"
+ },
+ {
+ "$ref": "#/definitions/markdown"
+ },
+ {
+ "$ref": "#/definitions/commit"
+ },
+ {
+ "$ref": "#/definitions/file_location"
+ },
+ {
+ "$ref": "#/definitions/module_location"
+ }
+ ]
+ },
+ "text_value": {
+ "type": "string"
+ },
+ "named_field": {
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "$ref": "#/definitions/text_value",
+ "minLength": 1
+ },
+ "description": {
+ "$ref": "#/definitions/text_value"
+ }
+ }
+ },
+ "named_list": {
+ "type": "object",
+ "description": "An object with named and typed fields",
+ "required": [
+ "type",
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "named-list"
+ },
+ "items": {
+ "type": "object",
+ "patternProperties": {
+ "^.*$": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/named_field"
+ },
+ {
+ "$ref": "#/definitions/detail_type"
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "list": {
+ "type": "object",
+ "description": "A list of typed fields",
+ "required": [
+ "type",
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "list"
+ },
+ "items": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ }
+ }
+ },
+ "table": {
+ "type": "object",
+ "description": "A table of typed fields",
+ "required": [
+ "type",
+ "rows"
+ ],
+ "properties": {
+ "type": {
+ "const": "table"
+ },
+ "header": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ },
+ "rows": {
+ "type": "array",
+ "items": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ }
+ }
+ }
+ },
+ "text": {
+ "type": "object",
+ "description": "Raw text",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "text"
+ },
+ "value": {
+ "$ref": "#/definitions/text_value"
+ }
+ }
+ },
+ "url": {
+ "type": "object",
+ "description": "A single URL",
+ "required": [
+ "type",
+ "href"
+ ],
+ "properties": {
+ "type": {
+ "const": "url"
+ },
+ "text": {
+ "$ref": "#/definitions/text_value"
+ },
+ "href": {
+ "type": "string",
+ "minLength": 1,
+ "examples": [
+ "http://mysite.com"
+ ]
+ }
+ }
+ },
+ "code": {
+ "type": "object",
+ "description": "A codeblock",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "code"
+ },
+ "value": {
+ "type": "string"
+ },
+ "lang": {
+ "type": "string",
+ "description": "A programming language"
+ }
+ }
+ },
+ "value": {
+ "type": "object",
+ "description": "A field that can store a range of types of value",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "value"
+ },
+ "value": {
+ "type": [
+ "number",
+ "string",
+ "boolean"
+ ]
+ }
+ }
+ },
+ "diff": {
+ "type": "object",
+ "description": "A diff",
+ "required": [
+ "type",
+ "before",
+ "after"
+ ],
+ "properties": {
+ "type": {
+ "const": "diff"
+ },
+ "before": {
+ "type": "string"
+ },
+ "after": {
+ "type": "string"
+ }
+ }
+ },
+ "markdown": {
+ "type": "object",
+ "description": "GitLab flavoured markdown, see https://docs.gitlab.com/ee/user/markdown.html",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "markdown"
+ },
+ "value": {
+ "$ref": "#/definitions/text_value",
+ "examples": [
+ "Here is markdown `inline code` #1 [test](gitlab.com)\n\n![GitLab Logo](https://about.gitlab.com/images/press/logo/preview/gitlab-logo-white-preview.png)"
+ ]
+ }
+ }
+ },
+ "commit": {
+ "type": "object",
+ "description": "A commit/tag/branch within the GitLab project",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "commit"
+ },
+ "value": {
+ "type": "string",
+ "description": "The commit SHA",
+ "minLength": 1
+ }
+ }
+ },
+ "file_location": {
+ "type": "object",
+ "description": "A location within a file in the project",
+ "required": [
+ "type",
+ "file_name",
+ "line_start"
+ ],
+ "properties": {
+ "type": {
+ "const": "file-location"
+ },
+ "file_name": {
+ "type": "string",
+ "minLength": 1
+ },
+ "line_start": {
+ "type": "integer"
+ },
+ "line_end": {
+ "type": "integer"
+ }
+ }
+ },
+ "module_location": {
+ "type": "object",
+ "description": "A location within a binary module of the form module+relative_offset",
+ "required": [
+ "type",
+ "module_name",
+ "offset"
+ ],
+ "properties": {
+ "type": {
+ "const": "module-location"
+ },
+ "module_name": {
+ "type": "string",
+ "minLength": 1,
+ "examples": [
+ "compiled_binary"
+ ]
+ },
+ "offset": {
+ "type": "integer",
+ "examples": [
+ 100
+ ]
+ }
+ }
+ }
+ },
+ "self": {
+ "version": "15.0.0"
+ },
+ "required": [
+ "scan",
+ "version",
+ "vulnerabilities"
+ ],
+ "additionalProperties": true,
+ "properties": {
+ "scan": {
+ "type": "object",
+ "required": [
+ "analyzer",
+ "end_time",
+ "scanner",
+ "start_time",
+ "status",
+ "type"
+ ],
+ "properties": {
+ "end_time": {
+ "type": "string",
+ "description": "ISO8601 UTC value with format yyyy-mm-ddThh:mm:ss, representing when the scan finished.",
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}$",
+ "examples": [
+ "2020-01-28T03:26:02"
+ ]
+ },
+ "messages": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "description": "Communication intended for the initiator of a scan.",
+ "required": [
+ "level",
+ "value"
+ ],
+ "properties": {
+ "level": {
+ "type": "string",
+ "description": "Describes the severity of the communication. Use info to communicate normal scan behaviour; warn to communicate a potentially recoverable problem, or a partial error; fatal to communicate an issue that causes the scan to halt.",
+ "enum": [
+ "info",
+ "warn",
+ "fatal"
+ ],
+ "examples": [
+ "info"
+ ]
+ },
+ "value": {
+ "type": "string",
+ "description": "The message to communicate.",
+ "minLength": 1,
+ "examples": [
+ "Permission denied, scanning aborted"
+ ]
+ }
+ }
+ }
+ },
+ "analyzer": {
+ "type": "object",
+ "description": "Object defining the analyzer used to perform the scan. Analyzers typically delegate to an underlying scanner to run the scan.",
+ "required": [
+ "id",
+ "name",
+ "version",
+ "vendor"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Unique id that identifies the analyzer.",
+ "minLength": 1,
+ "examples": [
+ "gitlab-dast"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "description": "A human readable value that identifies the analyzer, not required to be unique.",
+ "minLength": 1,
+ "examples": [
+ "GitLab DAST"
+ ]
+ },
+ "url": {
+ "type": "string",
+ "pattern": "^https?://.+",
+ "description": "A link to more information about the analyzer.",
+ "examples": [
+ "https://docs.gitlab.com/ee/user/application_security/dast"
+ ]
+ },
+ "vendor": {
+ "description": "The vendor/maintainer of the analyzer.",
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The name of the vendor.",
+ "minLength": 1,
+ "examples": [
+ "GitLab"
+ ]
+ }
+ }
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the analyzer.",
+ "minLength": 1,
+ "examples": [
+ "1.0.2"
+ ]
+ }
+ }
+ },
+ "scanner": {
+ "type": "object",
+ "description": "Object defining the scanner used to perform the scan.",
+ "required": [
+ "id",
+ "name",
+ "version",
+ "vendor"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Unique id that identifies the scanner.",
+ "minLength": 1,
+ "examples": [
+ "my-sast-scanner"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "description": "A human readable value that identifies the scanner, not required to be unique.",
+ "minLength": 1,
+ "examples": [
+ "My SAST Scanner"
+ ]
+ },
+ "url": {
+ "type": "string",
+ "description": "A link to more information about the scanner.",
+ "examples": [
+ "https://scanner.url"
+ ]
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the scanner.",
+ "minLength": 1,
+ "examples": [
+ "1.0.2"
+ ]
+ },
+ "vendor": {
+ "description": "The vendor/maintainer of the scanner.",
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The name of the vendor.",
+ "minLength": 1,
+ "examples": [
+ "GitLab"
+ ]
+ }
+ }
+ }
+ }
+ },
+ "start_time": {
+ "type": "string",
+ "description": "ISO8601 UTC value with format yyyy-mm-ddThh:mm:ss, representing when the scan started.",
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}$",
+ "examples": [
+ "2020-02-14T16:01:59"
+ ]
+ },
+ "status": {
+ "type": "string",
+ "description": "Result of the scan.",
+ "enum": [
+ "success",
+ "failure"
+ ]
+ },
+ "type": {
+ "type": "string",
+ "description": "Type of the scan.",
+ "enum": [
+ "sast"
+ ]
+ }
+ }
+ },
+ "schema": {
+ "type": "string",
+ "description": "URI pointing to the validating security report schema.",
+ "pattern": "^https?://.+"
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the schema to which the JSON report conforms.",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ },
+ "vulnerabilities": {
+ "type": "array",
+ "description": "Array of vulnerability objects.",
+ "items": {
+ "type": "object",
+ "description": "Describes the vulnerability using GitLab Flavored Markdown",
+ "required": [
+ "id",
+ "identifiers",
+ "location"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Unique identifier of the vulnerability. This is recommended to be a UUID.",
+ "examples": [
+ "642735a5-1425-428d-8d4e-3c854885a3c9"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "maxLength": 255,
+ "description": "The name of the vulnerability. This must not include the finding's specific information."
+ },
+ "description": {
+ "type": "string",
+ "maxLength": 1048576,
+ "description": "A long text section describing the vulnerability more fully."
+ },
+ "severity": {
+ "type": "string",
+ "description": "How much the vulnerability impacts the software. Possible values are Info, Unknown, Low, Medium, High, or Critical. Note that some analyzers may not report all these possible values.",
+ "enum": [
+ "Info",
+ "Unknown",
+ "Low",
+ "Medium",
+ "High",
+ "Critical"
+ ]
+ },
+ "solution": {
+ "type": "string",
+ "maxLength": 7000,
+ "description": "Explanation of how to fix the vulnerability."
+ },
+ "identifiers": {
+ "type": "array",
+ "minItems": 1,
+ "description": "An ordered array of references that identify a vulnerability on internal or external databases. The first identifier is the Primary Identifier, which has special meaning.",
+ "items": {
+ "type": "object",
+ "required": [
+ "type",
+ "name",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "for example, cve, cwe, osvdb, usn, or an analyzer-dependent type such as gemnasium).",
+ "minLength": 1
+ },
+ "name": {
+ "type": "string",
+ "description": "Human-readable name of the identifier.",
+ "minLength": 1
+ },
+ "url": {
+ "type": "string",
+ "description": "URL of the identifier's documentation.",
+ "pattern": "^https?://.+"
+ },
+ "value": {
+ "type": "string",
+ "description": "Value of the identifier, for matching purpose.",
+ "minLength": 1
+ }
+ }
+ }
+ },
+ "links": {
+ "type": "array",
+ "description": "An array of references to external documentation or articles that describe the vulnerability.",
+ "items": {
+ "type": "object",
+ "required": [
+ "url"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Name of the vulnerability details link."
+ },
+ "url": {
+ "type": "string",
+ "description": "URL of the vulnerability details document.",
+ "pattern": "^https?://.+"
+ }
+ }
+ }
+ },
+ "details": {
+ "$ref": "#/definitions/named_list/properties/items"
+ },
+ "tracking": {
+ "description": "Describes how this vulnerability should be tracked as the project changes.",
+ "oneOf": [
+ {
+ "description": "Declares that a series of items should be tracked using source-specific tracking methods.",
+ "required": [
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "source"
+ },
+ "items": {
+ "type": "array",
+ "items": {
+ "description": "An item that should be tracked using source-specific tracking methods.",
+ "type": "object",
+ "required": [
+ "signatures"
+ ],
+ "properties": {
+ "file": {
+ "type": "string",
+ "description": "Path to the file where the vulnerability is located."
+ },
+ "start_line": {
+ "type": "number",
+ "description": "The first line of the file that includes the vulnerability."
+ },
+ "end_line": {
+ "type": "number",
+ "description": "The last line of the file that includes the vulnerability."
+ },
+ "signatures": {
+ "type": "array",
+ "description": "An array of calculated tracking signatures for this tracking item.",
+ "minItems": 1,
+ "items": {
+ "description": "A calculated tracking signature value and metadata.",
+ "required": [
+ "algorithm",
+ "value"
+ ],
+ "properties": {
+ "algorithm": {
+ "type": "string",
+ "description": "The algorithm used to generate the signature."
+ },
+ "value": {
+ "type": "string",
+ "description": "The result of this signature algorithm."
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "Each tracking type must declare its own type."
+ }
+ }
+ },
+ "flags": {
+ "description": "Flags that can be attached to vulnerabilities.",
+ "type": "array",
+ "items": {
+ "type": "object",
+ "description": "Informational flags identified and assigned to a vulnerability.",
+ "required": [
+ "type",
+ "origin",
+ "description"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Result of the scan.",
+ "enum": [
+ "flagged-as-likely-false-positive"
+ ]
+ },
+ "origin": {
+ "minLength": 1,
+ "description": "Tool that issued the flag.",
+ "type": "string"
+ },
+ "description": {
+ "minLength": 1,
+ "description": "What the flag is about.",
+ "type": "string"
+ }
+ }
+ }
+ },
+ "location": {
+ "type": "object",
+ "description": "Identifies the vulnerability's location.",
+ "properties": {
+ "file": {
+ "type": "string",
+ "description": "Path to the file where the vulnerability is located."
+ },
+ "start_line": {
+ "type": "number",
+ "description": "The first line of the code affected by the vulnerability."
+ },
+ "end_line": {
+ "type": "number",
+ "description": "The last line of the code affected by the vulnerability."
+ },
+ "class": {
+ "type": "string",
+ "description": "Provides the name of the class where the vulnerability is located."
+ },
+ "method": {
+ "type": "string",
+ "description": "Provides the name of the method where the vulnerability is located."
+ }
+ }
+ },
+ "raw_source_code_extract": {
+ "type": "string",
+ "description": "Provides an unsanitized excerpt of the affected source code."
+ }
+ }
+ }
+ },
+ "remediations": {
+ "type": "array",
+ "description": "An array of objects containing information on available remediations, along with patch diffs to apply.",
+ "items": {
+ "type": "object",
+ "required": [
+ "fixes",
+ "summary",
+ "diff"
+ ],
+ "properties": {
+ "fixes": {
+ "type": "array",
+ "description": "An array of strings that represent references to vulnerabilities fixed by this remediation.",
+ "items": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Unique identifier of the vulnerability. This is recommended to be a UUID.",
+ "examples": [
+ "642735a5-1425-428d-8d4e-3c854885a3c9"
+ ]
+ }
+ }
+ }
+ },
+ "summary": {
+ "type": "string",
+ "minLength": 1,
+ "description": "An overview of how the vulnerabilities were fixed."
+ },
+ "diff": {
+ "type": "string",
+ "minLength": 1,
+ "description": "A base64-encoded remediation code diff, compatible with git apply."
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/lib/gitlab/ci/parsers/security/validators/schemas/15.0.0/secret-detection-report-format.json b/lib/gitlab/ci/parsers/security/validators/schemas/15.0.0/secret-detection-report-format.json
new file mode 100644
index 00000000000..fca00e17f26
--- /dev/null
+++ b/lib/gitlab/ci/parsers/security/validators/schemas/15.0.0/secret-detection-report-format.json
@@ -0,0 +1,854 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "https://gitlab.com/gitlab-org/security-products/security-report-schemas/-/raw/master/dist/secret-detection-report-format.json",
+ "title": "Report format for GitLab Secret Detection",
+ "description": "This schema provides the the report format for the Secret Detection analyzer (https://docs.gitlab.com/ee/user/application_security/secret_detection)",
+ "definitions": {
+ "detail_type": {
+ "oneOf": [
+ {
+ "$ref": "#/definitions/named_list"
+ },
+ {
+ "$ref": "#/definitions/list"
+ },
+ {
+ "$ref": "#/definitions/table"
+ },
+ {
+ "$ref": "#/definitions/text"
+ },
+ {
+ "$ref": "#/definitions/url"
+ },
+ {
+ "$ref": "#/definitions/code"
+ },
+ {
+ "$ref": "#/definitions/value"
+ },
+ {
+ "$ref": "#/definitions/diff"
+ },
+ {
+ "$ref": "#/definitions/markdown"
+ },
+ {
+ "$ref": "#/definitions/commit"
+ },
+ {
+ "$ref": "#/definitions/file_location"
+ },
+ {
+ "$ref": "#/definitions/module_location"
+ }
+ ]
+ },
+ "text_value": {
+ "type": "string"
+ },
+ "named_field": {
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "$ref": "#/definitions/text_value",
+ "minLength": 1
+ },
+ "description": {
+ "$ref": "#/definitions/text_value"
+ }
+ }
+ },
+ "named_list": {
+ "type": "object",
+ "description": "An object with named and typed fields",
+ "required": [
+ "type",
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "named-list"
+ },
+ "items": {
+ "type": "object",
+ "patternProperties": {
+ "^.*$": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/named_field"
+ },
+ {
+ "$ref": "#/definitions/detail_type"
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "list": {
+ "type": "object",
+ "description": "A list of typed fields",
+ "required": [
+ "type",
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "list"
+ },
+ "items": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ }
+ }
+ },
+ "table": {
+ "type": "object",
+ "description": "A table of typed fields",
+ "required": [
+ "type",
+ "rows"
+ ],
+ "properties": {
+ "type": {
+ "const": "table"
+ },
+ "header": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ },
+ "rows": {
+ "type": "array",
+ "items": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ }
+ }
+ }
+ },
+ "text": {
+ "type": "object",
+ "description": "Raw text",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "text"
+ },
+ "value": {
+ "$ref": "#/definitions/text_value"
+ }
+ }
+ },
+ "url": {
+ "type": "object",
+ "description": "A single URL",
+ "required": [
+ "type",
+ "href"
+ ],
+ "properties": {
+ "type": {
+ "const": "url"
+ },
+ "text": {
+ "$ref": "#/definitions/text_value"
+ },
+ "href": {
+ "type": "string",
+ "minLength": 1,
+ "examples": [
+ "http://mysite.com"
+ ]
+ }
+ }
+ },
+ "code": {
+ "type": "object",
+ "description": "A codeblock",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "code"
+ },
+ "value": {
+ "type": "string"
+ },
+ "lang": {
+ "type": "string",
+ "description": "A programming language"
+ }
+ }
+ },
+ "value": {
+ "type": "object",
+ "description": "A field that can store a range of types of value",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "value"
+ },
+ "value": {
+ "type": [
+ "number",
+ "string",
+ "boolean"
+ ]
+ }
+ }
+ },
+ "diff": {
+ "type": "object",
+ "description": "A diff",
+ "required": [
+ "type",
+ "before",
+ "after"
+ ],
+ "properties": {
+ "type": {
+ "const": "diff"
+ },
+ "before": {
+ "type": "string"
+ },
+ "after": {
+ "type": "string"
+ }
+ }
+ },
+ "markdown": {
+ "type": "object",
+ "description": "GitLab flavoured markdown, see https://docs.gitlab.com/ee/user/markdown.html",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "markdown"
+ },
+ "value": {
+ "$ref": "#/definitions/text_value",
+ "examples": [
+ "Here is markdown `inline code` #1 [test](gitlab.com)\n\n![GitLab Logo](https://about.gitlab.com/images/press/logo/preview/gitlab-logo-white-preview.png)"
+ ]
+ }
+ }
+ },
+ "commit": {
+ "type": "object",
+ "description": "A commit/tag/branch within the GitLab project",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "commit"
+ },
+ "value": {
+ "type": "string",
+ "description": "The commit SHA",
+ "minLength": 1
+ }
+ }
+ },
+ "file_location": {
+ "type": "object",
+ "description": "A location within a file in the project",
+ "required": [
+ "type",
+ "file_name",
+ "line_start"
+ ],
+ "properties": {
+ "type": {
+ "const": "file-location"
+ },
+ "file_name": {
+ "type": "string",
+ "minLength": 1
+ },
+ "line_start": {
+ "type": "integer"
+ },
+ "line_end": {
+ "type": "integer"
+ }
+ }
+ },
+ "module_location": {
+ "type": "object",
+ "description": "A location within a binary module of the form module+relative_offset",
+ "required": [
+ "type",
+ "module_name",
+ "offset"
+ ],
+ "properties": {
+ "type": {
+ "const": "module-location"
+ },
+ "module_name": {
+ "type": "string",
+ "minLength": 1,
+ "examples": [
+ "compiled_binary"
+ ]
+ },
+ "offset": {
+ "type": "integer",
+ "examples": [
+ 100
+ ]
+ }
+ }
+ }
+ },
+ "self": {
+ "version": "15.0.0"
+ },
+ "required": [
+ "scan",
+ "version",
+ "vulnerabilities"
+ ],
+ "additionalProperties": true,
+ "properties": {
+ "scan": {
+ "type": "object",
+ "required": [
+ "analyzer",
+ "end_time",
+ "scanner",
+ "start_time",
+ "status",
+ "type"
+ ],
+ "properties": {
+ "end_time": {
+ "type": "string",
+ "description": "ISO8601 UTC value with format yyyy-mm-ddThh:mm:ss, representing when the scan finished.",
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}$",
+ "examples": [
+ "2020-01-28T03:26:02"
+ ]
+ },
+ "messages": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "description": "Communication intended for the initiator of a scan.",
+ "required": [
+ "level",
+ "value"
+ ],
+ "properties": {
+ "level": {
+ "type": "string",
+ "description": "Describes the severity of the communication. Use info to communicate normal scan behaviour; warn to communicate a potentially recoverable problem, or a partial error; fatal to communicate an issue that causes the scan to halt.",
+ "enum": [
+ "info",
+ "warn",
+ "fatal"
+ ],
+ "examples": [
+ "info"
+ ]
+ },
+ "value": {
+ "type": "string",
+ "description": "The message to communicate.",
+ "minLength": 1,
+ "examples": [
+ "Permission denied, scanning aborted"
+ ]
+ }
+ }
+ }
+ },
+ "analyzer": {
+ "type": "object",
+ "description": "Object defining the analyzer used to perform the scan. Analyzers typically delegate to an underlying scanner to run the scan.",
+ "required": [
+ "id",
+ "name",
+ "version",
+ "vendor"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Unique id that identifies the analyzer.",
+ "minLength": 1,
+ "examples": [
+ "gitlab-dast"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "description": "A human readable value that identifies the analyzer, not required to be unique.",
+ "minLength": 1,
+ "examples": [
+ "GitLab DAST"
+ ]
+ },
+ "url": {
+ "type": "string",
+ "pattern": "^https?://.+",
+ "description": "A link to more information about the analyzer.",
+ "examples": [
+ "https://docs.gitlab.com/ee/user/application_security/dast"
+ ]
+ },
+ "vendor": {
+ "description": "The vendor/maintainer of the analyzer.",
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The name of the vendor.",
+ "minLength": 1,
+ "examples": [
+ "GitLab"
+ ]
+ }
+ }
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the analyzer.",
+ "minLength": 1,
+ "examples": [
+ "1.0.2"
+ ]
+ }
+ }
+ },
+ "scanner": {
+ "type": "object",
+ "description": "Object defining the scanner used to perform the scan.",
+ "required": [
+ "id",
+ "name",
+ "version",
+ "vendor"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Unique id that identifies the scanner.",
+ "minLength": 1,
+ "examples": [
+ "my-sast-scanner"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "description": "A human readable value that identifies the scanner, not required to be unique.",
+ "minLength": 1,
+ "examples": [
+ "My SAST Scanner"
+ ]
+ },
+ "url": {
+ "type": "string",
+ "description": "A link to more information about the scanner.",
+ "examples": [
+ "https://scanner.url"
+ ]
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the scanner.",
+ "minLength": 1,
+ "examples": [
+ "1.0.2"
+ ]
+ },
+ "vendor": {
+ "description": "The vendor/maintainer of the scanner.",
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The name of the vendor.",
+ "minLength": 1,
+ "examples": [
+ "GitLab"
+ ]
+ }
+ }
+ }
+ }
+ },
+ "start_time": {
+ "type": "string",
+ "description": "ISO8601 UTC value with format yyyy-mm-ddThh:mm:ss, representing when the scan started.",
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}$",
+ "examples": [
+ "2020-02-14T16:01:59"
+ ]
+ },
+ "status": {
+ "type": "string",
+ "description": "Result of the scan.",
+ "enum": [
+ "success",
+ "failure"
+ ]
+ },
+ "type": {
+ "type": "string",
+ "description": "Type of the scan.",
+ "enum": [
+ "secret_detection"
+ ]
+ }
+ }
+ },
+ "schema": {
+ "type": "string",
+ "description": "URI pointing to the validating security report schema.",
+ "pattern": "^https?://.+"
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the schema to which the JSON report conforms.",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ },
+ "vulnerabilities": {
+ "type": "array",
+ "description": "Array of vulnerability objects.",
+ "items": {
+ "type": "object",
+ "description": "Describes the vulnerability using GitLab Flavored Markdown",
+ "required": [
+ "id",
+ "identifiers",
+ "location"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Unique identifier of the vulnerability. This is recommended to be a UUID.",
+ "examples": [
+ "642735a5-1425-428d-8d4e-3c854885a3c9"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "maxLength": 255,
+ "description": "The name of the vulnerability. This must not include the finding's specific information."
+ },
+ "description": {
+ "type": "string",
+ "maxLength": 1048576,
+ "description": "A long text section describing the vulnerability more fully."
+ },
+ "severity": {
+ "type": "string",
+ "description": "How much the vulnerability impacts the software. Possible values are Info, Unknown, Low, Medium, High, or Critical. Note that some analyzers may not report all these possible values.",
+ "enum": [
+ "Info",
+ "Unknown",
+ "Low",
+ "Medium",
+ "High",
+ "Critical"
+ ]
+ },
+ "solution": {
+ "type": "string",
+ "maxLength": 7000,
+ "description": "Explanation of how to fix the vulnerability."
+ },
+ "identifiers": {
+ "type": "array",
+ "minItems": 1,
+ "description": "An ordered array of references that identify a vulnerability on internal or external databases. The first identifier is the Primary Identifier, which has special meaning.",
+ "items": {
+ "type": "object",
+ "required": [
+ "type",
+ "name",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "for example, cve, cwe, osvdb, usn, or an analyzer-dependent type such as gemnasium).",
+ "minLength": 1
+ },
+ "name": {
+ "type": "string",
+ "description": "Human-readable name of the identifier.",
+ "minLength": 1
+ },
+ "url": {
+ "type": "string",
+ "description": "URL of the identifier's documentation.",
+ "pattern": "^https?://.+"
+ },
+ "value": {
+ "type": "string",
+ "description": "Value of the identifier, for matching purpose.",
+ "minLength": 1
+ }
+ }
+ }
+ },
+ "links": {
+ "type": "array",
+ "description": "An array of references to external documentation or articles that describe the vulnerability.",
+ "items": {
+ "type": "object",
+ "required": [
+ "url"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Name of the vulnerability details link."
+ },
+ "url": {
+ "type": "string",
+ "description": "URL of the vulnerability details document.",
+ "pattern": "^https?://.+"
+ }
+ }
+ }
+ },
+ "details": {
+ "$ref": "#/definitions/named_list/properties/items"
+ },
+ "tracking": {
+ "description": "Describes how this vulnerability should be tracked as the project changes.",
+ "oneOf": [
+ {
+ "description": "Declares that a series of items should be tracked using source-specific tracking methods.",
+ "required": [
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "source"
+ },
+ "items": {
+ "type": "array",
+ "items": {
+ "description": "An item that should be tracked using source-specific tracking methods.",
+ "type": "object",
+ "required": [
+ "signatures"
+ ],
+ "properties": {
+ "file": {
+ "type": "string",
+ "description": "Path to the file where the vulnerability is located."
+ },
+ "start_line": {
+ "type": "number",
+ "description": "The first line of the file that includes the vulnerability."
+ },
+ "end_line": {
+ "type": "number",
+ "description": "The last line of the file that includes the vulnerability."
+ },
+ "signatures": {
+ "type": "array",
+ "description": "An array of calculated tracking signatures for this tracking item.",
+ "minItems": 1,
+ "items": {
+ "description": "A calculated tracking signature value and metadata.",
+ "required": [
+ "algorithm",
+ "value"
+ ],
+ "properties": {
+ "algorithm": {
+ "type": "string",
+ "description": "The algorithm used to generate the signature."
+ },
+ "value": {
+ "type": "string",
+ "description": "The result of this signature algorithm."
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "Each tracking type must declare its own type."
+ }
+ }
+ },
+ "flags": {
+ "description": "Flags that can be attached to vulnerabilities.",
+ "type": "array",
+ "items": {
+ "type": "object",
+ "description": "Informational flags identified and assigned to a vulnerability.",
+ "required": [
+ "type",
+ "origin",
+ "description"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Result of the scan.",
+ "enum": [
+ "flagged-as-likely-false-positive"
+ ]
+ },
+ "origin": {
+ "minLength": 1,
+ "description": "Tool that issued the flag.",
+ "type": "string"
+ },
+ "description": {
+ "minLength": 1,
+ "description": "What the flag is about.",
+ "type": "string"
+ }
+ }
+ }
+ },
+ "location": {
+ "required": [
+ "commit"
+ ],
+ "properties": {
+ "file": {
+ "type": "string",
+ "description": "Path to the file where the vulnerability is located"
+ },
+ "commit": {
+ "type": "object",
+ "description": "Represents the commit in which the vulnerability was detected",
+ "required": [
+ "sha"
+ ],
+ "properties": {
+ "author": {
+ "type": "string"
+ },
+ "date": {
+ "type": "string"
+ },
+ "message": {
+ "type": "string"
+ },
+ "sha": {
+ "type": "string",
+ "minLength": 1
+ }
+ }
+ },
+ "start_line": {
+ "type": "number",
+ "description": "The first line of the code affected by the vulnerability"
+ },
+ "end_line": {
+ "type": "number",
+ "description": "The last line of the code affected by the vulnerability"
+ },
+ "class": {
+ "type": "string",
+ "description": "Provides the name of the class where the vulnerability is located"
+ },
+ "method": {
+ "type": "string",
+ "description": "Provides the name of the method where the vulnerability is located"
+ }
+ }
+ },
+ "raw_source_code_extract": {
+ "type": "string",
+ "description": "Provides an unsanitized excerpt of the affected source code."
+ }
+ }
+ }
+ },
+ "remediations": {
+ "type": "array",
+ "description": "An array of objects containing information on available remediations, along with patch diffs to apply.",
+ "items": {
+ "type": "object",
+ "required": [
+ "fixes",
+ "summary",
+ "diff"
+ ],
+ "properties": {
+ "fixes": {
+ "type": "array",
+ "description": "An array of strings that represent references to vulnerabilities fixed by this remediation.",
+ "items": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Unique identifier of the vulnerability. This is recommended to be a UUID.",
+ "examples": [
+ "642735a5-1425-428d-8d4e-3c854885a3c9"
+ ]
+ }
+ }
+ }
+ },
+ "summary": {
+ "type": "string",
+ "minLength": 1,
+ "description": "An overview of how the vulnerabilities were fixed."
+ },
+ "diff": {
+ "type": "string",
+ "minLength": 1,
+ "description": "A base64-encoded remediation code diff, compatible with git apply."
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/lib/gitlab/ci/parsers/test/junit.rb b/lib/gitlab/ci/parsers/test/junit.rb
index 999ffff85d2..d95ecff85cd 100644
--- a/lib/gitlab/ci/parsers/test/junit.rb
+++ b/lib/gitlab/ci/parsers/test/junit.rb
@@ -8,7 +8,9 @@ module Gitlab
JunitParserError = Class.new(Gitlab::Ci::Parsers::ParserError)
ATTACHMENT_TAG_REGEX = /\[\[ATTACHMENT\|(?<path>.+?)\]\]/.freeze
- def parse!(xml_data, test_suite, job:)
+ def parse!(xml_data, test_report, job:)
+ test_suite = test_report.get_suite(job.test_suite_name)
+
root = Hash.from_xml(xml_data)
total_parsed = 0
max_test_cases = job.max_test_cases_per_report
diff --git a/lib/gitlab/ci/pipeline/chain/assign_partition.rb b/lib/gitlab/ci/pipeline/chain/assign_partition.rb
new file mode 100644
index 00000000000..4b8efe13d44
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/chain/assign_partition.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Pipeline
+ module Chain
+ class AssignPartition < Chain::Base
+ include Chain::Helpers
+
+ def perform!
+ @pipeline.partition_id = find_partition_id
+ end
+
+ def break?
+ @pipeline.errors.any?
+ end
+
+ private
+
+ def find_partition_id
+ if @command.creates_child_pipeline?
+ @command.parent_pipeline_partition_id
+ else
+ ::Ci::Pipeline.current_partition_value
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/pipeline/chain/command.rb b/lib/gitlab/ci/pipeline/chain/command.rb
index 0a6f6fd740c..14c320f77bf 100644
--- a/lib/gitlab/ci/pipeline/chain/command.rb
+++ b/lib/gitlab/ci/pipeline/chain/command.rb
@@ -80,6 +80,10 @@ module Gitlab
bridge&.parent_pipeline
end
+ def parent_pipeline_partition_id
+ parent_pipeline.partition_id if creates_child_pipeline?
+ end
+
def creates_child_pipeline?
bridge&.triggers_child_pipeline?
end
@@ -117,8 +121,14 @@ module Gitlab
end
def observe_jobs_count_in_alive_pipelines
+ jobs_count = if Feature.enabled?(:ci_limit_active_jobs_early, project)
+ project.all_pipelines.jobs_count_in_alive_pipelines
+ else
+ project.all_pipelines.builds_count_in_alive_pipelines
+ end
+
metrics.active_jobs_histogram
- .observe({ plan: project.actual_plan_name }, project.all_pipelines.jobs_count_in_alive_pipelines)
+ .observe({ plan: project.actual_plan_name }, jobs_count)
end
def increment_pipeline_failure_reason_counter(reason)
diff --git a/lib/gitlab/ci/pipeline/chain/config/content.rb b/lib/gitlab/ci/pipeline/chain/config/content.rb
index 3c150ca26bb..a14dec48619 100644
--- a/lib/gitlab/ci/pipeline/chain/config/content.rb
+++ b/lib/gitlab/ci/pipeline/chain/config/content.rb
@@ -7,6 +7,7 @@ module Gitlab
module Config
class Content < Chain::Base
include Chain::Helpers
+ include ::Gitlab::Utils::StrongMemoize
SOURCES = [
Gitlab::Ci::Pipeline::Chain::Config::Content::Parameter,
@@ -18,10 +19,10 @@ module Gitlab
].freeze
def perform!
- if config = find_config
- @pipeline.build_pipeline_config(content: config.content)
- @command.config_content = config.content
- @pipeline.config_source = config.source
+ if pipeline_config&.exists?
+ @pipeline.build_pipeline_config(content: pipeline_config.content)
+ @command.config_content = pipeline_config.content
+ @pipeline.config_source = pipeline_config.source
else
error('Missing CI config file')
end
@@ -33,7 +34,19 @@ module Gitlab
private
- def find_config
+ def pipeline_config
+ strong_memoize(:pipeline_config) do
+ next legacy_find_config if ::Feature.disabled?(:ci_project_pipeline_config_refactoring, project)
+
+ ::Gitlab::Ci::ProjectConfig.new(
+ project: project, sha: @pipeline.sha,
+ custom_content: @command.content,
+ pipeline_source: @command.source, pipeline_source_bridge: @command.bridge
+ )
+ end
+ end
+
+ def legacy_find_config
sources.each do |source|
config = source.new(@pipeline, @command)
return config if config.exists?
diff --git a/lib/gitlab/ci/pipeline/chain/config/content/source.rb b/lib/gitlab/ci/pipeline/chain/config/content/source.rb
index 8bc172f93d3..69dca1568b6 100644
--- a/lib/gitlab/ci/pipeline/chain/config/content/source.rb
+++ b/lib/gitlab/ci/pipeline/chain/config/content/source.rb
@@ -6,6 +6,7 @@ module Gitlab
module Chain
module Config
class Content
+ # When removing ci_project_pipeline_config_refactoring, this and its subclasses will be removed.
class Source
include Gitlab::Utils::StrongMemoize
diff --git a/lib/gitlab/ci/pipeline/chain/ensure_environments.rb b/lib/gitlab/ci/pipeline/chain/ensure_environments.rb
index 245ef32f06b..3dd9b85d9b2 100644
--- a/lib/gitlab/ci/pipeline/chain/ensure_environments.rb
+++ b/lib/gitlab/ci/pipeline/chain/ensure_environments.rb
@@ -18,7 +18,9 @@ module Gitlab
def ensure_environment(build)
return unless build.instance_of?(::Ci::Build) && build.has_environment?
- environment = ::Gitlab::Ci::Pipeline::Seed::Environment.new(build).to_resource
+ environment = ::Gitlab::Ci::Pipeline::Seed::Environment
+ .new(build, merge_request: @command.merge_request)
+ .to_resource
if environment.persisted?
build.persisted_environment = environment
diff --git a/lib/gitlab/ci/pipeline/chain/validate/external.rb b/lib/gitlab/ci/pipeline/chain/validate/external.rb
index 6e95c7988fc..915e48828d2 100644
--- a/lib/gitlab/ci/pipeline/chain/validate/external.rb
+++ b/lib/gitlab/ci/pipeline/chain/validate/external.rb
@@ -57,7 +57,8 @@ module Gitlab
}.compact
Gitlab::HTTP.post(
- validation_service_url, timeout: validation_service_timeout,
+ validation_service_url,
+ timeout: validation_service_timeout,
headers: headers,
body: validation_service_payload.to_json
)
@@ -96,13 +97,17 @@ module Gitlab
last_sign_in_ip: current_user.last_sign_in_ip,
sign_in_count: current_user.sign_in_count
},
+ credit_card: {
+ similar_cards_count: current_user.credit_card_validation&.similar_records&.count.to_i,
+ similar_holder_names_count: current_user.credit_card_validation&.similar_holder_names_count.to_i
+ },
pipeline: {
sha: pipeline.sha,
ref: pipeline.ref,
type: pipeline.source
},
builds: builds_validation_payload,
- total_builds_count: current_user.pipelines.jobs_count_in_alive_pipelines
+ total_builds_count: current_user.pipelines.builds_count_in_alive_pipelines
}
end
diff --git a/lib/gitlab/ci/pipeline/seed/build.rb b/lib/gitlab/ci/pipeline/seed/build.rb
index 93106b96af2..2e4267e986b 100644
--- a/lib/gitlab/ci/pipeline/seed/build.rb
+++ b/lib/gitlab/ci/pipeline/seed/build.rb
@@ -148,7 +148,9 @@ module Gitlab
ref: @pipeline.ref,
tag: @pipeline.tag,
trigger_request: @pipeline.legacy_trigger,
- protected: @pipeline.protected_ref?
+ protected: @pipeline.protected_ref?,
+ partition_id: @pipeline.partition_id,
+ metadata_attributes: { partition_id: @pipeline.partition_id }
}
end
diff --git a/lib/gitlab/ci/pipeline/seed/environment.rb b/lib/gitlab/ci/pipeline/seed/environment.rb
index 6bcc71a808b..8353bc523bf 100644
--- a/lib/gitlab/ci/pipeline/seed/environment.rb
+++ b/lib/gitlab/ci/pipeline/seed/environment.rb
@@ -5,17 +5,21 @@ module Gitlab
module Pipeline
module Seed
class Environment < Seed::Base
- attr_reader :job
+ attr_reader :job, :merge_request
- def initialize(job)
+ delegate :simple_variables, to: :job
+
+ def initialize(job, merge_request: nil)
@job = job
+ @merge_request = merge_request
end
def to_resource
environments.safe_find_or_create_by(name: expanded_environment_name) do |environment|
# Initialize the attributes at creation
- environment.auto_stop_in = auto_stop_in
+ environment.auto_stop_in = expanded_auto_stop_in
environment.tier = deployment_tier
+ environment.merge_request = merge_request
end
end
@@ -36,6 +40,12 @@ module Gitlab
def expanded_environment_name
job.expanded_environment_name
end
+
+ def expanded_auto_stop_in
+ return unless auto_stop_in
+
+ ExpandVariables.expand(auto_stop_in, -> { simple_variables.sort_and_expand_all })
+ end
end
end
end
diff --git a/lib/gitlab/ci/pipeline/seed/stage.rb b/lib/gitlab/ci/pipeline/seed/stage.rb
index 7cf6466cf4b..1c4247bd5ee 100644
--- a/lib/gitlab/ci/pipeline/seed/stage.rb
+++ b/lib/gitlab/ci/pipeline/seed/stage.rb
@@ -25,7 +25,8 @@ module Gitlab
{ name: @attributes.fetch(:name),
position: @attributes.fetch(:index),
pipeline: @pipeline,
- project: @pipeline.project }
+ project: @pipeline.project,
+ partition_id: @pipeline.partition_id }
end
def seeds
diff --git a/lib/gitlab/ci/processable_object_hierarchy.rb b/lib/gitlab/ci/processable_object_hierarchy.rb
new file mode 100644
index 00000000000..1122361e27e
--- /dev/null
+++ b/lib/gitlab/ci/processable_object_hierarchy.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ class ProcessableObjectHierarchy < ::Gitlab::ObjectHierarchy
+ private
+
+ def middle_table
+ ::Ci::BuildNeed.arel_table
+ end
+
+ def from_tables(cte)
+ [objects_table, cte.table, middle_table]
+ end
+
+ def parent_id_column(_cte)
+ middle_table[:name]
+ end
+
+ def ancestor_conditions(cte)
+ middle_table[:name].eq(objects_table[:name]).and(
+ middle_table[:build_id].eq(cte.table[:id])
+ )
+ end
+
+ def descendant_conditions(cte)
+ middle_table[:build_id].eq(objects_table[:id]).and(
+ middle_table[:name].eq(cte.table[:name])
+ )
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/project_config.rb b/lib/gitlab/ci/project_config.rb
new file mode 100644
index 00000000000..ded6877ef29
--- /dev/null
+++ b/lib/gitlab/ci/project_config.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ # Locates project CI config
+ class ProjectConfig
+ # The order of sources is important:
+ # - EE uses Compliance first since it must be used first if compliance templates are enabled.
+ # (see ee/lib/ee/gitlab/ci/project_config.rb)
+ # - Parameter is used by on-demand security scanning which passes the actual CI YAML to use as argument.
+ # - Bridge is used for downstream pipelines since the config is defined in the bridge job. If lower in priority,
+ # it would evaluate the project's YAML file instead.
+ # - Repository / ExternalProject / Remote: their order is not important between each other.
+ # - AutoDevops is used as default option if nothing else is found and if AutoDevops is enabled.
+ SOURCES = [
+ ProjectConfig::Parameter,
+ ProjectConfig::Bridge,
+ ProjectConfig::Repository,
+ ProjectConfig::ExternalProject,
+ ProjectConfig::Remote,
+ ProjectConfig::AutoDevops
+ ].freeze
+
+ def initialize(project:, sha:, custom_content: nil, pipeline_source: nil, pipeline_source_bridge: nil)
+ @config = find_config(project, sha, custom_content, pipeline_source, pipeline_source_bridge)
+ end
+
+ delegate :content, :source, to: :@config, allow_nil: true
+
+ def exists?
+ !!@config&.exists?
+ end
+
+ private
+
+ def find_config(project, sha, custom_content, pipeline_source, pipeline_source_bridge)
+ sources.each do |source|
+ config = source.new(project, sha, custom_content, pipeline_source, pipeline_source_bridge)
+ return config if config.exists?
+ end
+
+ nil
+ end
+
+ def sources
+ SOURCES
+ end
+ end
+ end
+end
+
+Gitlab::Ci::ProjectConfig.prepend_mod_with('Gitlab::Ci::ProjectConfig')
diff --git a/lib/gitlab/ci/project_config/auto_devops.rb b/lib/gitlab/ci/project_config/auto_devops.rb
new file mode 100644
index 00000000000..c6905f480a2
--- /dev/null
+++ b/lib/gitlab/ci/project_config/auto_devops.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ class ProjectConfig
+ class AutoDevops < Source
+ def content
+ strong_memoize(:content) do
+ next unless project&.auto_devops_enabled?
+
+ template = Gitlab::Template::GitlabCiYmlTemplate.find(template_name)
+ YAML.dump('include' => [{ 'template' => template.full_name }])
+ end
+ end
+
+ def source
+ :auto_devops_source
+ end
+
+ private
+
+ def template_name
+ 'Auto-DevOps'
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/project_config/bridge.rb b/lib/gitlab/ci/project_config/bridge.rb
new file mode 100644
index 00000000000..c342ab2c215
--- /dev/null
+++ b/lib/gitlab/ci/project_config/bridge.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ class ProjectConfig
+ class Bridge < Source
+ def content
+ return unless pipeline_source_bridge
+
+ pipeline_source_bridge.yaml_for_downstream
+ end
+
+ def source
+ :bridge_source
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/project_config/external_project.rb b/lib/gitlab/ci/project_config/external_project.rb
new file mode 100644
index 00000000000..0ed5d6fa226
--- /dev/null
+++ b/lib/gitlab/ci/project_config/external_project.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ class ProjectConfig
+ class ExternalProject < Source
+ def content
+ strong_memoize(:content) do
+ next unless external_project_path?
+
+ path_file, path_project, ref = extract_location_tokens
+
+ config_location = { 'project' => path_project, 'file' => path_file }
+ config_location['ref'] = ref if ref.present?
+
+ YAML.dump('include' => [config_location])
+ end
+ end
+
+ def source
+ :external_project_source
+ end
+
+ private
+
+ # Example: path/to/.gitlab-ci.yml@another-group/another-project
+ def external_project_path?
+ ci_config_path =~ /\A.+(yml|yaml)@.+\z/
+ end
+
+ # Example: path/to/.gitlab-ci.yml@another-group/another-project:refname
+ def extract_location_tokens
+ path_file, path_project = ci_config_path.split('@', 2)
+
+ if path_project.include? ":"
+ project, ref = path_project.split(':', 2)
+ [path_file, project, ref]
+ else
+ [path_file, path_project]
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/project_config/parameter.rb b/lib/gitlab/ci/project_config/parameter.rb
new file mode 100644
index 00000000000..69e699c27f1
--- /dev/null
+++ b/lib/gitlab/ci/project_config/parameter.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ class ProjectConfig
+ class Parameter < Source
+ def content
+ strong_memoize(:content) do
+ next unless custom_content.present?
+
+ custom_content
+ end
+ end
+
+ def source
+ :parameter_source
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/project_config/remote.rb b/lib/gitlab/ci/project_config/remote.rb
new file mode 100644
index 00000000000..cf1292706d2
--- /dev/null
+++ b/lib/gitlab/ci/project_config/remote.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ class ProjectConfig
+ class Remote < Source
+ def content
+ strong_memoize(:content) do
+ next unless ci_config_path =~ URI::DEFAULT_PARSER.make_regexp(%w[http https])
+
+ YAML.dump('include' => [{ 'remote' => ci_config_path }])
+ end
+ end
+
+ def source
+ :remote_source
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/project_config/repository.rb b/lib/gitlab/ci/project_config/repository.rb
new file mode 100644
index 00000000000..435ad4d42fe
--- /dev/null
+++ b/lib/gitlab/ci/project_config/repository.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ class ProjectConfig
+ class Repository < Source
+ def content
+ strong_memoize(:content) do
+ next unless file_in_repository?
+
+ YAML.dump('include' => [{ 'local' => ci_config_path }])
+ end
+ end
+
+ def source
+ :repository_source
+ end
+
+ private
+
+ def file_in_repository?
+ return unless project
+ return unless sha
+
+ project.repository.gitlab_ci_yml_for(sha, ci_config_path).present?
+ rescue GRPC::NotFound, GRPC::Internal
+ nil
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/project_config/source.rb b/lib/gitlab/ci/project_config/source.rb
new file mode 100644
index 00000000000..ebe5728163b
--- /dev/null
+++ b/lib/gitlab/ci/project_config/source.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ class ProjectConfig
+ class Source
+ include Gitlab::Utils::StrongMemoize
+
+ def initialize(project, sha, custom_content, pipeline_source, pipeline_source_bridge)
+ @project = project
+ @sha = sha
+ @custom_content = custom_content
+ @pipeline_source = pipeline_source
+ @pipeline_source_bridge = pipeline_source_bridge
+ end
+
+ def exists?
+ strong_memoize(:exists) do
+ content.present?
+ end
+ end
+
+ def content
+ raise NotImplementedError
+ end
+
+ def source
+ raise NotImplementedError
+ end
+
+ private
+
+ attr_reader :project, :sha, :custom_content, :pipeline_source, :pipeline_source_bridge
+
+ def ci_config_path
+ @ci_config_path ||= project.ci_config_path_or_default
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/reports/coverage_report_generator.rb b/lib/gitlab/ci/reports/coverage_report_generator.rb
index 6d57e05aa63..88b3b14d5c9 100644
--- a/lib/gitlab/ci/reports/coverage_report_generator.rb
+++ b/lib/gitlab/ci/reports/coverage_report_generator.rb
@@ -35,7 +35,7 @@ module Gitlab
private
def report_builds
- @pipeline.latest_report_builds_in_self_and_descendants(::Ci::JobArtifact.coverage_reports)
+ @pipeline.latest_report_builds_in_self_and_project_descendants(::Ci::JobArtifact.of_report_type(:coverage))
end
end
end
diff --git a/lib/gitlab/ci/reports/sbom/component.rb b/lib/gitlab/ci/reports/sbom/component.rb
index 86b9be274cc..198b34451b4 100644
--- a/lib/gitlab/ci/reports/sbom/component.rb
+++ b/lib/gitlab/ci/reports/sbom/component.rb
@@ -7,10 +7,10 @@ module Gitlab
class Component
attr_reader :component_type, :name, :version
- def initialize(component = {})
- @component_type = component['type']
- @name = component['name']
- @version = component['version']
+ def initialize(type:, name:, version:)
+ @component_type = type
+ @name = name
+ @version = version
end
end
end
diff --git a/lib/gitlab/ci/reports/sbom/report.rb b/lib/gitlab/ci/reports/sbom/report.rb
index dc6b3153e51..4f84d12f78c 100644
--- a/lib/gitlab/ci/reports/sbom/report.rb
+++ b/lib/gitlab/ci/reports/sbom/report.rb
@@ -17,11 +17,11 @@ module Gitlab
end
def set_source(source)
- self.source = Source.new(source)
+ self.source = source
end
def add_component(component)
- components << Component.new(component)
+ components << component
end
private
diff --git a/lib/gitlab/ci/reports/sbom/source.rb b/lib/gitlab/ci/reports/sbom/source.rb
index 60bf30b65a5..ea0fb8d4fbb 100644
--- a/lib/gitlab/ci/reports/sbom/source.rb
+++ b/lib/gitlab/ci/reports/sbom/source.rb
@@ -7,10 +7,10 @@ module Gitlab
class Source
attr_reader :source_type, :data, :fingerprint
- def initialize(source = {})
- @source_type = source['type']
- @data = source['data']
- @fingerprint = source['fingerprint']
+ def initialize(type:, data:, fingerprint:)
+ @source_type = type
+ @data = data
+ @fingerprint = fingerprint
end
end
end
diff --git a/lib/gitlab/ci/reports/security/scanner.rb b/lib/gitlab/ci/reports/security/scanner.rb
index 1ac66a0c671..918df163ede 100644
--- a/lib/gitlab/ci/reports/security/scanner.rb
+++ b/lib/gitlab/ci/reports/security/scanner.rb
@@ -7,13 +7,13 @@ module Gitlab
class Scanner
ANALYZER_ORDER = {
"bundler_audit" => 1,
- "retire.js" => 2,
+ "retire.js" => 2,
"gemnasium" => 3,
"gemnasium-maven" => 3,
"gemnasium-python" => 3,
"bandit" => 1,
"spotbugs" => 1,
- "semgrep" => 2
+ "semgrep" => 2
}.freeze
attr_accessor :external_id, :name, :vendor, :version
diff --git a/lib/gitlab/ci/status/build/failed.rb b/lib/gitlab/ci/status/build/failed.rb
index 5d60aa8f540..a136044c124 100644
--- a/lib/gitlab/ci/status/build/failed.rb
+++ b/lib/gitlab/ci/status/build/failed.rb
@@ -31,6 +31,7 @@ module Gitlab
downstream_pipeline_creation_failed: 'downstream pipeline can not be created',
secrets_provider_not_found: 'secrets provider can not be found',
reached_max_descendant_pipelines_depth: 'reached maximum depth of child pipelines',
+ reached_max_pipeline_hierarchy_size: 'downstream pipeline tree is too large',
project_deleted: 'pipeline project was deleted',
user_blocked: 'pipeline user was blocked',
ci_quota_exceeded: 'no more CI minutes available',
@@ -39,7 +40,8 @@ module Gitlab
builds_disabled: 'project builds are disabled',
environment_creation_failure: 'environment creation failure',
deployment_rejected: 'deployment rejected',
- ip_restriction_failure: 'IP address restriction failure'
+ ip_restriction_failure: 'IP address restriction failure',
+ failed_outdated_deployment_job: 'failed outdated deployment job'
}.freeze
private_constant :REASONS
diff --git a/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml
index f0ddc4b4916..539e1a6385d 100644
--- a/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml
@@ -1,5 +1,5 @@
variables:
- DAST_AUTO_DEPLOY_IMAGE_VERSION: 'v2.33.0'
+ DAST_AUTO_DEPLOY_IMAGE_VERSION: 'v2.37.0'
.dast-auto-deploy:
image: "${CI_TEMPLATE_REGISTRY_HOST}/gitlab-org/cluster-integration/auto-deploy-image:${DAST_AUTO_DEPLOY_IMAGE_VERSION}"
diff --git a/lib/gitlab/ci/templates/Jobs/Dependency-Scanning.latest.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Dependency-Scanning.latest.gitlab-ci.yml
new file mode 100644
index 00000000000..70f85382967
--- /dev/null
+++ b/lib/gitlab/ci/templates/Jobs/Dependency-Scanning.latest.gitlab-ci.yml
@@ -0,0 +1,244 @@
+# To contribute improvements to CI/CD templates, please follow the Development guide at:
+# https://docs.gitlab.com/ee/development/cicd/templates.html
+# This specific template is located at:
+# https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Jobs/Dependency-Scanning.gitlab-ci.yml
+
+# Read more about this feature here: https://docs.gitlab.com/ee/user/application_security/dependency_scanning/
+#
+# Configure dependency scanning with CI/CD variables (https://docs.gitlab.com/ee/ci/variables/index.html).
+# List of available variables: https://docs.gitlab.com/ee/user/application_security/dependency_scanning/index.html#available-variables
+
+variables:
+ # Setting this variable will affect all Security templates
+ # (SAST, Dependency Scanning, ...)
+ SECURE_ANALYZERS_PREFIX: "$CI_TEMPLATE_REGISTRY_HOST/security-products"
+ DS_EXCLUDED_ANALYZERS: ""
+ DS_EXCLUDED_PATHS: "spec, test, tests, tmp"
+ DS_MAJOR_VERSION: 3
+
+dependency_scanning:
+ stage: test
+ script:
+ - echo "$CI_JOB_NAME is used for configuration only, and its script should not be executed"
+ - exit 1
+ artifacts:
+ reports:
+ dependency_scanning: gl-dependency-scanning-report.json
+ dependencies: []
+ rules:
+ - when: never
+
+.ds-analyzer:
+ extends: dependency_scanning
+ allow_failure: true
+ variables:
+ # DS_ANALYZER_IMAGE is an undocumented variable used internally to allow QA to
+ # override the analyzer image with a custom value. This may be subject to change or
+ # breakage across GitLab releases.
+ DS_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/$DS_ANALYZER_NAME:$DS_MAJOR_VERSION"
+ # DS_ANALYZER_NAME is an undocumented variable used in job definitions
+ # to inject the analyzer name in the image name.
+ DS_ANALYZER_NAME: ""
+ image:
+ name: "$DS_ANALYZER_IMAGE$DS_IMAGE_SUFFIX"
+ # `rules` must be overridden explicitly by each child job
+ # see https://gitlab.com/gitlab-org/gitlab/-/issues/218444
+ script:
+ - /analyzer run
+
+.cyclonedx-reports:
+ artifacts:
+ paths:
+ - "**/gl-sbom-*.cdx.json"
+
+.gemnasium-shared-rule:
+ exists:
+ - '{Gemfile.lock,*/Gemfile.lock,*/*/Gemfile.lock}'
+ - '{composer.lock,*/composer.lock,*/*/composer.lock}'
+ - '{gems.locked,*/gems.locked,*/*/gems.locked}'
+ - '{go.sum,*/go.sum,*/*/go.sum}'
+ - '{npm-shrinkwrap.json,*/npm-shrinkwrap.json,*/*/npm-shrinkwrap.json}'
+ - '{package-lock.json,*/package-lock.json,*/*/package-lock.json}'
+ - '{yarn.lock,*/yarn.lock,*/*/yarn.lock}'
+ - '{packages.lock.json,*/packages.lock.json,*/*/packages.lock.json}'
+ - '{conan.lock,*/conan.lock,*/*/conan.lock}'
+
+gemnasium-dependency_scanning:
+ extends:
+ - .ds-analyzer
+ - .cyclonedx-reports
+ variables:
+ DS_ANALYZER_NAME: "gemnasium"
+ GEMNASIUM_LIBRARY_SCAN_ENABLED: "true"
+ rules:
+ - if: $DEPENDENCY_SCANNING_DISABLED
+ when: never
+ - if: $DS_EXCLUDED_ANALYZERS =~ /gemnasium([^-]|$)/
+ when: never
+
+ # Add the job to merge request pipelines if there's an open merge request.
+ - if: $CI_PIPELINE_SOURCE == "merge_request_event" &&
+ $GITLAB_FEATURES =~ /\bdependency_scanning\b/ &&
+ $CI_GITLAB_FIPS_MODE == "true"
+ exists: !reference [.gemnasium-shared-rule, exists]
+ variables:
+ DS_IMAGE_SUFFIX: "-fips"
+ DS_REMEDIATE: "false"
+ - if: $CI_PIPELINE_SOURCE == "merge_request_event" &&
+ $GITLAB_FEATURES =~ /\bdependency_scanning\b/
+ exists: !reference [.gemnasium-shared-rule, exists]
+
+ # Don't add it to a *branch* pipeline if it's already in a merge request pipeline.
+ - if: $CI_OPEN_MERGE_REQUESTS
+ when: never
+
+ # Add the job to branch pipelines.
+ - if: $CI_COMMIT_BRANCH &&
+ $GITLAB_FEATURES =~ /\bdependency_scanning\b/ &&
+ $CI_GITLAB_FIPS_MODE == "true"
+ exists: !reference [.gemnasium-shared-rule, exists]
+ variables:
+ DS_IMAGE_SUFFIX: "-fips"
+ DS_REMEDIATE: "false"
+ - if: $CI_COMMIT_BRANCH &&
+ $GITLAB_FEATURES =~ /\bdependency_scanning\b/
+ exists: !reference [.gemnasium-shared-rule, exists]
+
+.gemnasium-maven-shared-rule:
+ exists:
+ - '{build.gradle,*/build.gradle,*/*/build.gradle}'
+ - '{build.gradle.kts,*/build.gradle.kts,*/*/build.gradle.kts}'
+ - '{build.sbt,*/build.sbt,*/*/build.sbt}'
+ - '{pom.xml,*/pom.xml,*/*/pom.xml}'
+
+gemnasium-maven-dependency_scanning:
+ extends:
+ - .ds-analyzer
+ - .cyclonedx-reports
+ variables:
+ DS_ANALYZER_NAME: "gemnasium-maven"
+ rules:
+ - if: $DEPENDENCY_SCANNING_DISABLED
+ when: never
+ - if: $DS_EXCLUDED_ANALYZERS =~ /gemnasium-maven/
+ when: never
+
+ # Add the job to merge request pipelines if there's an open merge request.
+ - if: $CI_PIPELINE_SOURCE == "merge_request_event" &&
+ $GITLAB_FEATURES =~ /\bdependency_scanning\b/ &&
+ $CI_GITLAB_FIPS_MODE == "true"
+ exists: !reference [.gemnasium-maven-shared-rule, exists]
+ variables:
+ DS_IMAGE_SUFFIX: "-fips"
+ DS_REMEDIATE: "false"
+ - if: $CI_PIPELINE_SOURCE == "merge_request_event" &&
+ $GITLAB_FEATURES =~ /\bdependency_scanning\b/
+ exists: !reference [.gemnasium-maven-shared-rule, exists]
+
+ # Don't add it to a *branch* pipeline if it's already in a merge request pipeline.
+ - if: $CI_OPEN_MERGE_REQUESTS
+ when: never
+
+ # Add the job to branch pipelines.
+ - if: $CI_COMMIT_BRANCH &&
+ $GITLAB_FEATURES =~ /\bdependency_scanning\b/ &&
+ $CI_GITLAB_FIPS_MODE == "true"
+ exists: !reference [.gemnasium-maven-shared-rule, exists]
+ variables:
+ DS_IMAGE_SUFFIX: "-fips"
+ - if: $CI_COMMIT_BRANCH &&
+ $GITLAB_FEATURES =~ /\bdependency_scanning\b/
+ exists: !reference [.gemnasium-maven-shared-rule, exists]
+
+.gemnasium-python-shared-rule:
+ exists:
+ - '{requirements.txt,*/requirements.txt,*/*/requirements.txt}'
+ - '{requirements.pip,*/requirements.pip,*/*/requirements.pip}'
+ - '{Pipfile,*/Pipfile,*/*/Pipfile}'
+ - '{requires.txt,*/requires.txt,*/*/requires.txt}'
+ - '{setup.py,*/setup.py,*/*/setup.py}'
+ - '{poetry.lock,*/poetry.lock,*/*/poetry.lock}'
+
+gemnasium-python-dependency_scanning:
+ extends:
+ - .ds-analyzer
+ - .cyclonedx-reports
+ variables:
+ DS_ANALYZER_NAME: "gemnasium-python"
+ rules:
+ - if: $DEPENDENCY_SCANNING_DISABLED
+ when: never
+ - if: $DS_EXCLUDED_ANALYZERS =~ /gemnasium-python/
+ when: never
+
+ # Add the job to merge request pipelines if there's an open merge request.
+ - if: $CI_PIPELINE_SOURCE == "merge_request_event" &&
+ $GITLAB_FEATURES =~ /\bdependency_scanning\b/ &&
+ $CI_GITLAB_FIPS_MODE == "true"
+ exists: !reference [.gemnasium-python-shared-rule, exists]
+ variables:
+ DS_IMAGE_SUFFIX: "-fips"
+ - if: $CI_PIPELINE_SOURCE == "merge_request_event" &&
+ $GITLAB_FEATURES =~ /\bdependency_scanning\b/
+ exists: !reference [.gemnasium-python-shared-rule, exists]
+ # Support passing of $PIP_REQUIREMENTS_FILE
+ # See https://docs.gitlab.com/ee/user/application_security/dependency_scanning/#configuring-specific-analyzers-used-by-dependency-scanning
+ - if: $CI_PIPELINE_SOURCE == "merge_request_event" &&
+ $GITLAB_FEATURES =~ /\bdependency_scanning\b/ &&
+ $PIP_REQUIREMENTS_FILE &&
+ $CI_GITLAB_FIPS_MODE == "true"
+ variables:
+ DS_IMAGE_SUFFIX: "-fips"
+ - if: $CI_PIPELINE_SOURCE == "merge_request_event" &&
+ $GITLAB_FEATURES =~ /\bdependency_scanning\b/ &&
+ $PIP_REQUIREMENTS_FILE
+
+ # Don't add it to a *branch* pipeline if it's already in a merge request pipeline.
+ - if: $CI_OPEN_MERGE_REQUESTS
+ when: never
+
+ # Add the job to branch pipelines.
+ - if: $CI_COMMIT_BRANCH &&
+ $GITLAB_FEATURES =~ /\bdependency_scanning\b/ &&
+ $CI_GITLAB_FIPS_MODE == "true"
+ exists: !reference [.gemnasium-python-shared-rule, exists]
+ variables:
+ DS_IMAGE_SUFFIX: "-fips"
+ - if: $CI_COMMIT_BRANCH &&
+ $GITLAB_FEATURES =~ /\bdependency_scanning\b/
+ exists: !reference [.gemnasium-python-shared-rule, exists]
+ # Support passing of $PIP_REQUIREMENTS_FILE
+ # See https://docs.gitlab.com/ee/user/application_security/dependency_scanning/#configuring-specific-analyzers-used-by-dependency-scanning
+ - if: $CI_COMMIT_BRANCH &&
+ $GITLAB_FEATURES =~ /\bdependency_scanning\b/ &&
+ $PIP_REQUIREMENTS_FILE &&
+ $CI_GITLAB_FIPS_MODE == "true"
+ variables:
+ DS_IMAGE_SUFFIX: "-fips"
+ - if: $CI_COMMIT_BRANCH &&
+ $GITLAB_FEATURES =~ /\bdependency_scanning\b/ &&
+ $PIP_REQUIREMENTS_FILE
+
+bundler-audit-dependency_scanning:
+ extends: .ds-analyzer
+ variables:
+ DS_ANALYZER_NAME: "bundler-audit"
+ DS_MAJOR_VERSION: 2
+ script:
+ - echo "This job was deprecated in GitLab 14.8 and removed in GitLab 15.0"
+ - echo "For more information see https://gitlab.com/gitlab-org/gitlab/-/issues/347491"
+ - exit 1
+ rules:
+ - when: never
+
+retire-js-dependency_scanning:
+ extends: .ds-analyzer
+ variables:
+ DS_ANALYZER_NAME: "retire.js"
+ DS_MAJOR_VERSION: 2
+ script:
+ - echo "This job was deprecated in GitLab 14.8 and removed in GitLab 15.0"
+ - echo "For more information see https://gitlab.com/gitlab-org/gitlab/-/issues/289830"
+ - exit 1
+ rules:
+ - when: never
diff --git a/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml
index 1a2a8b4edb4..78fe108e8b9 100644
--- a/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml
@@ -1,5 +1,5 @@
variables:
- AUTO_DEPLOY_IMAGE_VERSION: 'v2.33.0'
+ AUTO_DEPLOY_IMAGE_VERSION: 'v2.37.0'
.auto-deploy:
image: "${CI_TEMPLATE_REGISTRY_HOST}/gitlab-org/cluster-integration/auto-deploy-image:${AUTO_DEPLOY_IMAGE_VERSION}"
diff --git a/lib/gitlab/ci/templates/Jobs/Deploy.latest.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Deploy.latest.gitlab-ci.yml
index cb8818357a2..bc2e1fed0d4 100644
--- a/lib/gitlab/ci/templates/Jobs/Deploy.latest.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/Deploy.latest.gitlab-ci.yml
@@ -1,5 +1,5 @@
variables:
- AUTO_DEPLOY_IMAGE_VERSION: 'v2.33.0'
+ AUTO_DEPLOY_IMAGE_VERSION: 'v2.37.0'
.auto-deploy:
image: "${CI_TEMPLATE_REGISTRY_HOST}/gitlab-org/cluster-integration/auto-deploy-image:${AUTO_DEPLOY_IMAGE_VERSION}"
diff --git a/lib/gitlab/ci/templates/Jobs/License-Scanning.latest.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/License-Scanning.latest.gitlab-ci.yml
new file mode 100644
index 00000000000..e47f669c2e2
--- /dev/null
+++ b/lib/gitlab/ci/templates/Jobs/License-Scanning.latest.gitlab-ci.yml
@@ -0,0 +1,48 @@
+# To contribute improvements to CI/CD templates, please follow the Development guide at:
+# https://docs.gitlab.com/ee/development/cicd/templates.html
+# This specific template is located at:
+# https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Jobs/License-Scanning.gitlab-ci.yml
+
+# Read more about this feature here: https://docs.gitlab.com/ee/user/compliance/license_compliance/index.html
+#
+# Configure license scanning with CI/CD variables (https://docs.gitlab.com/ee/ci/variables/index.html).
+# List of available variables: https://docs.gitlab.com/ee/user/compliance/license_compliance/#available-variables
+
+variables:
+ # Setting this variable will affect all Security templates
+ # (SAST, Dependency Scanning, ...)
+ SECURE_ANALYZERS_PREFIX: "$CI_TEMPLATE_REGISTRY_HOST/security-products"
+
+ LICENSE_MANAGEMENT_SETUP_CMD: '' # If needed, specify a command to setup your environment with a custom package manager.
+ LICENSE_MANAGEMENT_VERSION: 4
+
+license_scanning:
+ stage: test
+ image:
+ name: "$SECURE_ANALYZERS_PREFIX/license-finder:$LICENSE_MANAGEMENT_VERSION"
+ entrypoint: [""]
+ variables:
+ LM_REPORT_VERSION: '2.1'
+ SETUP_CMD: $LICENSE_MANAGEMENT_SETUP_CMD
+ allow_failure: true
+ script:
+ - /run.sh analyze .
+ artifacts:
+ reports:
+ license_scanning: gl-license-scanning-report.json
+ dependencies: []
+ rules:
+ - if: $LICENSE_MANAGEMENT_DISABLED
+ when: never
+
+ # Add the job to merge request pipelines if there's an open merge request.
+ - if: $CI_PIPELINE_SOURCE == "merge_request_event" &&
+ $GITLAB_FEATURES =~ /\blicense_scanning\b/
+
+ # Don't add it to a *branch* pipeline if it's already in a merge request pipeline.
+ - if: $CI_OPEN_MERGE_REQUESTS
+ when: never
+
+ # Add the job to branch pipelines.
+ - if: $CI_COMMIT_BRANCH &&
+ $GITLAB_FEATURES =~ /\blicense_scanning\b/
diff --git a/lib/gitlab/ci/templates/Jobs/SAST.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/SAST.gitlab-ci.yml
index dd164c00724..a6d47e31de2 100644
--- a/lib/gitlab/ci/templates/Jobs/SAST.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/SAST.gitlab-ci.yml
@@ -36,19 +36,12 @@ sast:
bandit-sast:
extends: .sast-analyzer
- image:
- name: "$SAST_ANALYZER_IMAGE"
- variables:
- SAST_ANALYZER_IMAGE_TAG: 2
- SAST_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/bandit:$SAST_ANALYZER_IMAGE_TAG"
+ script:
+ - echo "This job was deprecated in GitLab 14.8 and removed in GitLab 15.4"
+ - echo "For more information see https://gitlab.com/gitlab-org/gitlab/-/issues/352554"
+ - exit 1
rules:
- - if: $SAST_DISABLED
- when: never
- - if: $SAST_EXCLUDED_ANALYZERS =~ /bandit/
- when: never
- - if: $CI_COMMIT_BRANCH
- exists:
- - '**/*.py'
+ - when: never
brakeman-sast:
extends: .sast-analyzer
@@ -69,23 +62,12 @@ brakeman-sast:
eslint-sast:
extends: .sast-analyzer
- image:
- name: "$SAST_ANALYZER_IMAGE"
- variables:
- SAST_ANALYZER_IMAGE_TAG: 2
- SAST_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/eslint:$SAST_ANALYZER_IMAGE_TAG"
+ script:
+ - echo "This job was deprecated in GitLab 14.8 and removed in GitLab 15.4"
+ - echo "For more information see https://gitlab.com/gitlab-org/gitlab/-/issues/352554"
+ - exit 1
rules:
- - if: $SAST_DISABLED
- when: never
- - if: $SAST_EXCLUDED_ANALYZERS =~ /eslint/
- when: never
- - if: $CI_COMMIT_BRANCH
- exists:
- - '**/*.html'
- - '**/*.js'
- - '**/*.jsx'
- - '**/*.ts'
- - '**/*.tsx'
+ - when: never
flawfinder-sast:
extends: .sast-analyzer
@@ -125,19 +107,12 @@ kubesec-sast:
gosec-sast:
extends: .sast-analyzer
- image:
- name: "$SAST_ANALYZER_IMAGE"
- variables:
- SAST_ANALYZER_IMAGE_TAG: 3
- SAST_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/gosec:$SAST_ANALYZER_IMAGE_TAG"
+ script:
+ - echo "This job was deprecated in GitLab 14.8 and removed in GitLab 15.4"
+ - echo "For more information see https://gitlab.com/gitlab-org/gitlab/-/issues/352554"
+ - exit 1
rules:
- - if: $SAST_DISABLED
- when: never
- - if: $SAST_EXCLUDED_ANALYZERS =~ /gosec/
- when: never
- - if: $CI_COMMIT_BRANCH
- exists:
- - '**/*.go'
+ - when: never
.mobsf-sast:
extends: .sast-analyzer
@@ -261,6 +236,8 @@ semgrep-sast:
- '**/*.c'
- '**/*.go'
- '**/*.java'
+ - '**/*.cs'
+ - '**/*.html'
sobelow-sast:
extends: .sast-analyzer
@@ -297,6 +274,5 @@ spotbugs-sast:
- if: $CI_COMMIT_BRANCH
exists:
- '**/*.groovy'
- - '**/*.java'
- '**/*.scala'
- '**/*.kt'
diff --git a/lib/gitlab/ci/templates/Jobs/SAST.latest.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/SAST.latest.gitlab-ci.yml
index c6938920ea4..c0ca821ebff 100644
--- a/lib/gitlab/ci/templates/Jobs/SAST.latest.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/SAST.latest.gitlab-ci.yml
@@ -36,24 +36,12 @@ sast:
bandit-sast:
extends: .sast-analyzer
- image:
- name: "$SAST_ANALYZER_IMAGE"
- variables:
- SAST_ANALYZER_IMAGE_TAG: 2
- SAST_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/bandit:$SAST_ANALYZER_IMAGE_TAG"
+ script:
+ - echo "This job was deprecated in GitLab 14.8 and removed in GitLab 15.3"
+ - echo "For more information see https://gitlab.com/gitlab-org/gitlab/-/issues/352554"
+ - exit 1
rules:
- - if: $SAST_DISABLED
- when: never
- - if: $SAST_EXCLUDED_ANALYZERS =~ /bandit/
- when: never
- - if: $CI_PIPELINE_SOURCE == "merge_request_event" # Add the job to merge request pipelines if there's an open merge request.
- exists:
- - '**/*.py'
- - if: $CI_OPEN_MERGE_REQUESTS # Don't add it to a *branch* pipeline if it's already in a merge request pipeline.
- when: never
- - if: $CI_COMMIT_BRANCH # If there's no open merge request, add it to a *branch* pipeline instead.
- exists:
- - '**/*.py'
+ - when: never
brakeman-sast:
extends: .sast-analyzer
@@ -80,32 +68,12 @@ brakeman-sast:
eslint-sast:
extends: .sast-analyzer
- image:
- name: "$SAST_ANALYZER_IMAGE"
- variables:
- SAST_ANALYZER_IMAGE_TAG: 2
- SAST_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/eslint:$SAST_ANALYZER_IMAGE_TAG"
+ script:
+ - echo "This job was deprecated in GitLab 14.8 and removed in GitLab 15.3"
+ - echo "For more information see https://gitlab.com/gitlab-org/gitlab/-/issues/352554"
+ - exit 1
rules:
- - if: $SAST_DISABLED
- when: never
- - if: $SAST_EXCLUDED_ANALYZERS =~ /eslint/
- when: never
- - if: $CI_PIPELINE_SOURCE == "merge_request_event" # Add the job to merge request pipelines if there's an open merge request.
- exists:
- - '**/*.html'
- - '**/*.js'
- - '**/*.jsx'
- - '**/*.ts'
- - '**/*.tsx'
- - if: $CI_OPEN_MERGE_REQUESTS # Don't add it to a *branch* pipeline if it's already in a merge request pipeline.
- when: never
- - if: $CI_COMMIT_BRANCH # If there's no open merge request, add it to a *branch* pipeline instead.
- exists:
- - '**/*.html'
- - '**/*.js'
- - '**/*.jsx'
- - '**/*.ts'
- - '**/*.tsx'
+ - when: never
flawfinder-sast:
extends: .sast-analyzer
@@ -138,6 +106,15 @@ flawfinder-sast:
- '**/*.cp'
- '**/*.cxx'
+gosec-sast:
+ extends: .sast-analyzer
+ script:
+ - echo "This job was deprecated in GitLab 15.0 and removed in GitLab 15.2"
+ - echo "For more information see https://gitlab.com/gitlab-org/gitlab/-/issues/352554"
+ - exit 1
+ rules:
+ - when: never
+
kubesec-sast:
extends: .sast-analyzer
image:
@@ -159,27 +136,6 @@ kubesec-sast:
- if: $CI_COMMIT_BRANCH &&
$SCAN_KUBERNETES_MANIFESTS == 'true'
-gosec-sast:
- extends: .sast-analyzer
- image:
- name: "$SAST_ANALYZER_IMAGE"
- variables:
- SAST_ANALYZER_IMAGE_TAG: 3
- SAST_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/gosec:$SAST_ANALYZER_IMAGE_TAG"
- rules:
- - if: $SAST_DISABLED
- when: never
- - if: $SAST_EXCLUDED_ANALYZERS =~ /gosec/
- when: never
- - if: $CI_PIPELINE_SOURCE == "merge_request_event" # Add the job to merge request pipelines if there's an open merge request.
- exists:
- - '**/*.go'
- - if: $CI_OPEN_MERGE_REQUESTS # Don't add it to a *branch* pipeline if it's already in a merge request pipeline.
- when: never
- - if: $CI_COMMIT_BRANCH # If there's no open merge request, add it to a *branch* pipeline instead.
- exists:
- - '**/*.go'
-
.mobsf-sast:
extends: .sast-analyzer
image:
@@ -323,7 +279,7 @@ semgrep-sast:
image:
name: "$SAST_ANALYZER_IMAGE"
variables:
- SERACH_MAX_DEPTH: 20
+ SEARCH_MAX_DEPTH: 20
SAST_ANALYZER_IMAGE_TAG: 3
SAST_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/semgrep:$SAST_ANALYZER_IMAGE_TAG$SAST_IMAGE_SUFFIX"
rules:
@@ -341,6 +297,8 @@ semgrep-sast:
- '**/*.c'
- '**/*.go'
- '**/*.java'
+ - '**/*.html'
+ - '**/*.cs'
- if: $CI_OPEN_MERGE_REQUESTS # Don't add it to a *branch* pipeline if it's already in a merge request pipeline.
when: never
- if: $CI_COMMIT_BRANCH # If there's no open merge request, add it to a *branch* pipeline instead.
@@ -353,6 +311,8 @@ semgrep-sast:
- '**/*.c'
- '**/*.go'
- '**/*.java'
+ - '**/*.html'
+ - '**/*.cs'
sobelow-sast:
extends: .sast-analyzer
@@ -394,7 +354,6 @@ spotbugs-sast:
- if: $CI_PIPELINE_SOURCE == "merge_request_event" # Add the job to merge request pipelines if there's an open merge request.
exists:
- '**/*.groovy'
- - '**/*.java'
- '**/*.scala'
- '**/*.kt'
- if: $CI_OPEN_MERGE_REQUESTS # Don't add it to a *branch* pipeline if it's already in a merge request pipeline.
@@ -402,6 +361,5 @@ spotbugs-sast:
- if: $CI_COMMIT_BRANCH # If there's no open merge request, add it to a *branch* pipeline instead.
exists:
- '**/*.groovy'
- - '**/*.java'
- '**/*.scala'
- '**/*.kt'
diff --git a/lib/gitlab/ci/templates/Katalon.gitlab-ci.yml b/lib/gitlab/ci/templates/Katalon.gitlab-ci.yml
new file mode 100644
index 00000000000..c8939c8f5a2
--- /dev/null
+++ b/lib/gitlab/ci/templates/Katalon.gitlab-ci.yml
@@ -0,0 +1,65 @@
+# This template is provided and maintained by Katalon, an official Technology Partner with GitLab.
+#
+# Use this template to run a Katalon Studio test from this repository.
+# You can:
+# - Copy and paste this template into a new `.gitlab-ci.yml` file.
+# - Add this template to an existing `.gitlab-ci.yml` file by using the `include:` keyword.
+#
+# In either case, you must also select which job you want to run, `.katalon_tests`
+# or `.katalon_tests_with_artifacts` (see configuration below), and add that configuration
+# to a new job with `extends:`. For example:
+#
+# Katalon-tests:
+# extends:
+# - .katalon_tests_with_artifacts
+#
+# Requirements:
+# - A Katalon Studio project with the content saved in the root GitLab repository folder.
+# - An active KRE license.
+# - A valid Katalon API key.
+#
+# CI/CD variables, set in the project CI/CD settings:
+# - KATALON_TEST_SUITE_PATH: The default path is `Test Suites/<Your Test Suite Name>`.
+# Defines which test suite to run.
+# - KATALON_API_KEY: The Katalon API key.
+# - KATALON_PROJECT_DIR: Optional. Add if the project is in another location.
+# - KATALON_ORG_ID: Optional. Add if you are part of multiple Katalon orgs.
+# Set to the Org ID that has KRE licenses assigned. For more info on the Org ID,
+# see https://support.katalon.com/hc/en-us/articles/4724459179545-How-to-get-Organization-ID-
+
+.katalon_tests:
+ # Use the latest version of the Katalon Runtime Engine. You can also use other versions of the
+ # Katalon Runtime Engine by specifying another tag, for example `katalonstudio/katalon:8.1.2`
+ # or `katalonstudio/katalon:8.3.0`.
+ image: 'katalonstudio/katalon'
+ services:
+ - docker:dind
+ variables:
+ # Specify the Katalon Studio project directory. By default, it is stored under the root project folder.
+ KATALON_PROJECT_DIR: $CI_PROJECT_DIR
+
+ # The following bash script has two different versions, one if you set the KATALON_ORG_ID
+ # CI/CD variable, and the other if you did not set it. If you have more than one org in
+ # admin.katalon.com you must set the KATALON_ORG_ID variable with an ORG ID or
+ # the Katalon Test Suite fails to run.
+ #
+ # You can update or add additional `katalonc` commands below. To see all of the arguments
+ # `katalonc` supports, go to https://docs.katalon.com/katalon-studio/docs/console-mode-execution.html
+ script:
+ - |-
+ if [[ $KATALON_ORG_ID == "" ]]; then
+ katalonc.sh -projectPath=$KATALON_PROJECT_DIR -apiKey=$KATALON_API_KEY -browserType="Chrome" -retry=0 -statusDelay=20 -testSuitePath="$KATALON_TEST_SUITE_PATH" -reportFolder=Reports/
+ else
+ katalonc.sh -projectPath=$KATALON_PROJECT_DIR -apiKey=$KATALON_API_KEY -browserType="Chrome" -retry=0 -statusDelay=20 -orgID=$KATALON_ORG_ID -testSuitePath="$KATALON_TEST_SUITE_PATH" -reportFolder=Reports/
+ fi
+
+# Upload the artifacts and make the junit report accessible under the Pipeline Tests
+.katalon_tests_with_artifacts:
+ extends: .katalon_tests
+ artifacts:
+ when: always
+ paths:
+ - Reports/
+ reports:
+ junit:
+ Reports/*/*/*/*.xml
diff --git a/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml
index 3d7883fb87a..79a08c33fdf 100644
--- a/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml
@@ -11,12 +11,12 @@
#
# Requirements:
# - A `test` stage to be present in the pipeline.
-# - You must define the image to be scanned in the DOCKER_IMAGE variable. If DOCKER_IMAGE is the
+# - You must define the image to be scanned in the CS_IMAGE variable. If CS_IMAGE is the
# same as $CI_APPLICATION_REPOSITORY:$CI_APPLICATION_TAG, you can skip this.
-# - Container registry credentials defined by `DOCKER_USER` and `DOCKER_PASSWORD` variables if the
+# - Container registry credentials defined by `CS_REGISTRY_USER` and `CS_REGISTRY_PASSWORD` variables if the
# image to be scanned is in a private registry.
# - For auto-remediation, a readable Dockerfile in the root of the project or as defined by the
-# DOCKERFILE_PATH variable.
+# CS_DOCKERFILE_PATH variable.
#
# Configure container scanning with CI/CD variables (https://docs.gitlab.com/ee/ci/variables/index.html).
# List of available variables: https://docs.gitlab.com/ee/user/application_security/container_scanning/#available-variables
diff --git a/lib/gitlab/ci/templates/Security/Container-Scanning.latest.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/Container-Scanning.latest.gitlab-ci.yml
new file mode 100644
index 00000000000..f7b1d12b3b3
--- /dev/null
+++ b/lib/gitlab/ci/templates/Security/Container-Scanning.latest.gitlab-ci.yml
@@ -0,0 +1,68 @@
+# To contribute improvements to CI/CD templates, please follow the Development guide at:
+# https://docs.gitlab.com/ee/development/cicd/templates.html
+# This specific template is located at:
+# https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml
+
+# Use this template to enable container scanning in your project.
+# You should add this template to an existing `.gitlab-ci.yml` file by using the `include:`
+# keyword.
+# The template should work without modifications but you can customize the template settings if
+# needed: https://docs.gitlab.com/ee/user/application_security/container_scanning/#customizing-the-container-scanning-settings
+#
+# Requirements:
+# - A `test` stage to be present in the pipeline.
+# - You must define the image to be scanned in the CS_IMAGE variable. If CS_IMAGE is the
+# same as $CI_APPLICATION_REPOSITORY:$CI_APPLICATION_TAG, you can skip this.
+# - Container registry credentials defined by `CS_REGISTRY_USER` and `CS_REGISTRY_PASSWORD` variables if the
+# image to be scanned is in a private registry.
+# - For auto-remediation, a readable Dockerfile in the root of the project or as defined by the
+# CS_DOCKERFILE_PATH variable.
+#
+# Configure container scanning with CI/CD variables (https://docs.gitlab.com/ee/ci/variables/index.html).
+# List of available variables: https://docs.gitlab.com/ee/user/application_security/container_scanning/#available-variables
+
+variables:
+ CS_ANALYZER_IMAGE: "$CI_TEMPLATE_REGISTRY_HOST/security-products/container-scanning:5"
+
+container_scanning:
+ image: "$CS_ANALYZER_IMAGE$CS_IMAGE_SUFFIX"
+ stage: test
+ variables:
+ # To provide a `vulnerability-allowlist.yml` file, override the GIT_STRATEGY variable in your
+ # `.gitlab-ci.yml` file and set it to `fetch`.
+ # For details, see the following links:
+ # https://docs.gitlab.com/ee/user/application_security/container_scanning/index.html#overriding-the-container-scanning-template
+ # https://docs.gitlab.com/ee/user/application_security/container_scanning/#vulnerability-allowlisting
+ GIT_STRATEGY: none
+ allow_failure: true
+ artifacts:
+ reports:
+ container_scanning: gl-container-scanning-report.json
+ dependency_scanning: gl-dependency-scanning-report.json
+ paths: [gl-container-scanning-report.json, gl-dependency-scanning-report.json]
+ dependencies: []
+ script:
+ - gtcs scan
+ rules:
+ - if: $CONTAINER_SCANNING_DISABLED
+ when: never
+
+ # Add the job to merge request pipelines if there's an open merge request.
+ - if: $CI_PIPELINE_SOURCE == "merge_request_event" &&
+ $CI_GITLAB_FIPS_MODE == "true" &&
+ $CS_ANALYZER_IMAGE !~ /-(fips|ubi)\z/
+ variables:
+ CS_IMAGE_SUFFIX: -fips
+ - if: $CI_PIPELINE_SOURCE == "merge_request_event"
+
+ # Don't add it to a *branch* pipeline if it's already in a merge request pipeline.
+ - if: $CI_OPEN_MERGE_REQUESTS
+ when: never
+
+ # Add the job to branch pipelines.
+ - if: $CI_COMMIT_BRANCH &&
+ $CI_GITLAB_FIPS_MODE == "true" &&
+ $CS_ANALYZER_IMAGE !~ /-(fips|ubi)\z/
+ variables:
+ CS_IMAGE_SUFFIX: -fips
+ - if: $CI_COMMIT_BRANCH
diff --git a/lib/gitlab/ci/templates/Terraform/Base.gitlab-ci.yml b/lib/gitlab/ci/templates/Terraform/Base.gitlab-ci.yml
index 3a956ebfc49..9a40a23b276 100644
--- a/lib/gitlab/ci/templates/Terraform/Base.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Terraform/Base.gitlab-ci.yml
@@ -9,7 +9,7 @@
# There is a more opinionated template which we suggest the users to abide,
# which is the lib/gitlab/ci/templates/Terraform.gitlab-ci.yml
image:
- name: "$CI_TEMPLATE_REGISTRY_HOST/gitlab-org/terraform-images/releases/terraform:1.1.9"
+ name: "$CI_TEMPLATE_REGISTRY_HOST/gitlab-org/terraform-images/releases/1.1:v0.43.0"
variables:
TF_ROOT: ${CI_PROJECT_DIR} # The relative path to the root directory of the Terraform project
diff --git a/lib/gitlab/ci/templates/npm.gitlab-ci.yml b/lib/gitlab/ci/templates/npm.gitlab-ci.yml
index 64c784f43cb..fb0d300338b 100644
--- a/lib/gitlab/ci/templates/npm.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/npm.gitlab-ci.yml
@@ -38,7 +38,7 @@ publish:
# Compare the version in package.json to all published versions.
# If the package.json version has not yet been published, run `npm publish`.
- |
- if [[ $(npm view "${NPM_PACKAGE_NAME}" versions) != *"'${NPM_PACKAGE_VERSION}'"* ]]; then
+ if [[ "$(npm view ${NPM_PACKAGE_NAME} versions)" != *"'${NPM_PACKAGE_VERSION}'"* ]]; then
npm publish
echo "Successfully published version ${NPM_PACKAGE_VERSION} of ${NPM_PACKAGE_NAME} to GitLab's NPM registry: ${CI_PROJECT_URL}/-/packages"
else
diff --git a/lib/gitlab/ci/trace.rb b/lib/gitlab/ci/trace.rb
index 95a60b852b8..c5664ef1cfb 100644
--- a/lib/gitlab/ci/trace.rb
+++ b/lib/gitlab/ci/trace.rb
@@ -23,7 +23,6 @@ module Gitlab
attr_reader :job
- delegate :old_trace, to: :job
delegate :can_attempt_archival_now?, :increment_archival_attempts!,
:archival_attempts_message, :archival_attempts_available?, to: :trace_metadata
@@ -82,7 +81,7 @@ module Gitlab
end
def live?
- job.trace_chunks.any? || current_path.present? || old_trace.present?
+ job.trace_chunks.any? || current_path.present?
end
def read(&block)
@@ -111,7 +110,6 @@ module Gitlab
# Erase the live trace
erase_trace_chunks!
FileUtils.rm_f(current_path) if current_path # Remove a trace file of a live trace
- job.erase_old_trace! if job.has_old_trace? # Remove a trace in database of a live trace
ensure
@current_path = nil
end
@@ -162,8 +160,6 @@ module Gitlab
Gitlab::Ci::Trace::ChunkedIO.new(job)
elsif current_path
File.open(current_path, "rb")
- elsif old_trace
- StringIO.new(old_trace)
end
end
@@ -210,11 +206,6 @@ module Gitlab
archive_stream!(stream)
FileUtils.rm(current_path)
end
- elsif old_trace
- StringIO.new(old_trace, 'rb').tap do |stream|
- archive_stream!(stream)
- job.erase_old_trace!
- end
end
end
diff --git a/lib/gitlab/ci/variables/builder.rb b/lib/gitlab/ci/variables/builder.rb
index 95dff83506d..528d72c9bcc 100644
--- a/lib/gitlab/ci/variables/builder.rb
+++ b/lib/gitlab/ci/variables/builder.rb
@@ -118,7 +118,7 @@ module Gitlab
def predefined_variables(job)
Gitlab::Ci::Variables::Collection.new.tap do |variables|
variables.append(key: 'CI_JOB_NAME', value: job.name)
- variables.append(key: 'CI_JOB_STAGE', value: job.stage)
+ variables.append(key: 'CI_JOB_STAGE', value: job.stage_name)
variables.append(key: 'CI_JOB_MANUAL', value: 'true') if job.action?
variables.append(key: 'CI_PIPELINE_TRIGGERED', value: 'true') if job.trigger_request
@@ -127,7 +127,7 @@ module Gitlab
# legacy variables
variables.append(key: 'CI_BUILD_NAME', value: job.name)
- variables.append(key: 'CI_BUILD_STAGE', value: job.stage)
+ variables.append(key: 'CI_BUILD_STAGE', value: job.stage_name)
variables.append(key: 'CI_BUILD_TRIGGERED', value: 'true') if job.trigger_request
variables.append(key: 'CI_BUILD_MANUAL', value: 'true') if job.action?
end
diff --git a/lib/gitlab/ci/variables/helpers.rb b/lib/gitlab/ci/variables/helpers.rb
index 7cc727bb3ea..16e3afd8620 100644
--- a/lib/gitlab/ci/variables/helpers.rb
+++ b/lib/gitlab/ci/variables/helpers.rb
@@ -6,24 +6,24 @@ module Gitlab
module Helpers
class << self
def merge_variables(current_vars, new_vars)
- current_vars = transform_from_yaml_variables(current_vars)
- new_vars = transform_from_yaml_variables(new_vars)
+ return current_vars if new_vars.blank?
- transform_to_yaml_variables(
- current_vars.merge(new_vars)
- )
- end
+ current_vars = transform_to_array(current_vars) if current_vars.is_a?(Hash)
+ new_vars = transform_to_array(new_vars) if new_vars.is_a?(Hash)
- def transform_to_yaml_variables(vars)
- vars.to_h.map do |key, value|
- { key: key.to_s, value: value, public: true }
- end
+ (new_vars + current_vars).uniq { |var| var[:key] }
end
- def transform_from_yaml_variables(vars)
- return vars.stringify_keys.transform_values(&:to_s) if vars.is_a?(Hash)
+ def transform_to_array(vars)
+ return [] if vars.blank?
- vars.to_a.to_h { |var| [var[:key].to_s, var[:value]] }
+ vars.map do |key, data|
+ if data.is_a?(Hash)
+ { key: key.to_s, **data.except(:key) }
+ else
+ { key: key.to_s, value: data }
+ end
+ end
end
def inherit_yaml_variables(from:, to:, inheritance:)
@@ -35,7 +35,7 @@ module Gitlab
def apply_inheritance(variables, inheritance)
case inheritance
when true then variables
- when false then {}
+ when false then []
when Array then variables.select { |var| inheritance.include?(var[:key]) }
end
end
diff --git a/lib/gitlab/ci/yaml_processor/feature_flags.rb b/lib/gitlab/ci/yaml_processor/feature_flags.rb
index f03db9d0e6b..50d37f6e4a0 100644
--- a/lib/gitlab/ci/yaml_processor/feature_flags.rb
+++ b/lib/gitlab/ci/yaml_processor/feature_flags.rb
@@ -5,10 +5,10 @@ module Gitlab
class YamlProcessor
module FeatureFlags
ACTOR_KEY = 'ci_yaml_processor_feature_flag_actor'
+ CORRECT_USAGE_KEY = 'ci_yaml_processor_feature_flag_correct_usage'
NO_ACTOR_VALUE = :no_actor
-
- NoActorError = Class.new(StandardError)
NO_ACTOR_MESSAGE = "Actor not set. Ensure to call `enabled?` inside `with_actor` block"
+ NoActorError = Class.new(StandardError)
class << self
# Cache a feature flag actor as thread local variable so
@@ -31,6 +31,15 @@ module Gitlab
::Feature.enabled?(feature_flag, current_actor)
end
+ def ensure_correct_usage
+ previous = Thread.current[CORRECT_USAGE_KEY]
+ Thread.current[CORRECT_USAGE_KEY] = true
+
+ yield
+ ensure
+ Thread.current[CORRECT_USAGE_KEY] = previous
+ end
+
private
def current_actor
@@ -39,10 +48,22 @@ module Gitlab
value
rescue NoActorError => e
- Gitlab::ErrorTracking.track_and_raise_for_dev_exception(e)
+ handle_missing_actor(e)
nil
end
+
+ def handle_missing_actor(exception)
+ if ensure_correct_usage?
+ Gitlab::ErrorTracking.track_and_raise_for_dev_exception(exception)
+ else
+ Gitlab::ErrorTracking.track_exception(exception)
+ end
+ end
+
+ def ensure_correct_usage?
+ Thread.current[CORRECT_USAGE_KEY] == true
+ end
end
end
end
diff --git a/lib/gitlab/ci/yaml_processor/result.rb b/lib/gitlab/ci/yaml_processor/result.rb
index 4bd1ac3b67f..f203f88442d 100644
--- a/lib/gitlab/ci/yaml_processor/result.rb
+++ b/lib/gitlab/ci/yaml_processor/result.rb
@@ -43,7 +43,7 @@ module Gitlab
end
def root_variables
- @root_variables ||= transform_to_yaml_variables(variables)
+ @root_variables ||= transform_to_array(variables)
end
def jobs
@@ -70,7 +70,7 @@ module Gitlab
environment: job[:environment_name],
coverage_regex: job[:coverage],
# yaml_variables is calculated with using job_variables in Seed::Build
- job_variables: transform_to_yaml_variables(job[:job_variables]),
+ job_variables: transform_to_array(job[:job_variables]),
root_variables_inheritance: job[:root_variables_inheritance],
needs_attributes: job.dig(:needs, :job),
interruptible: job[:interruptible],
@@ -114,7 +114,7 @@ module Gitlab
Gitlab::Ci::Variables::Helpers.inherit_yaml_variables(
from: root_variables,
- to: transform_to_yaml_variables(job[:job_variables]),
+ to: job[:job_variables],
inheritance: job.fetch(:root_variables_inheritance, true)
)
end
@@ -137,8 +137,8 @@ module Gitlab
job[:release]
end
- def transform_to_yaml_variables(variables)
- ::Gitlab::Ci::Variables::Helpers.transform_to_yaml_variables(variables)
+ def transform_to_array(variables)
+ ::Gitlab::Ci::Variables::Helpers.transform_to_array(variables)
end
end
end
diff --git a/lib/gitlab/cleanup/personal_access_tokens.rb b/lib/gitlab/cleanup/personal_access_tokens.rb
new file mode 100644
index 00000000000..a1e4b5765c2
--- /dev/null
+++ b/lib/gitlab/cleanup/personal_access_tokens.rb
@@ -0,0 +1,105 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Cleanup
+ class PersonalAccessTokens
+ # By default tokens that haven't been used for over 1 year will be revoked
+ DEFAULT_TIME_PERIOD = 1.year
+ # To prevent inadvertently revoking all tokens, we provide a minimum time
+ MINIMUM_TIME_PERIOD = 1.day
+
+ attr_reader :logger, :cut_off_date, :revocation_time, :group
+
+ def initialize(cut_off_date: DEFAULT_TIME_PERIOD.ago.beginning_of_day, logger: nil, group_full_path:)
+ @cut_off_date = cut_off_date
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ @group = Group.find_by_full_path(group_full_path)
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ raise "Group with full_path #{group_full_path} not found" unless @group
+ raise "Invalid time: #{@cut_off_date}" unless @cut_off_date <= MINIMUM_TIME_PERIOD.ago
+
+ # Use a static revocation time to make correlation of revoked
+ # tokens easier, should it be needed.
+ @revocation_time = Time.current.utc
+ @logger = logger || Gitlab::AppJsonLogger
+
+ raise "Invalid logger: #{@logger}" unless @logger.respond_to?(:info) && @logger.respond_to?(:warn)
+ end
+
+ def run!(dry_run: true, revoke_active_tokens: false)
+ # rubocop:disable Rails/Output
+ if dry_run
+ puts "Dry running. No changes will be made"
+ elsif revoke_active_tokens
+ puts "Revoking used and unused access tokens created before #{cut_off_date}..."
+ else
+ puts "Revoking access tokens last used and created before #{cut_off_date}..."
+ end
+ # rubocop:enable Rails/Output
+
+ tokens_to_revoke = revocable_tokens(revoke_active_tokens)
+
+ # rubocop:disable Cop/InBatches
+ tokens_to_revoke.in_batches do |access_tokens|
+ revoke_batch(access_tokens, dry_run)
+ end
+ # rubocop:enable Cop/InBatches
+ end
+
+ private
+
+ def revocable_tokens(revoke_active_tokens)
+ if revoke_active_tokens
+ PersonalAccessToken
+ .active
+ .owner_is_human
+ .created_before(cut_off_date)
+ .for_users(group.users)
+ else
+ PersonalAccessToken
+ .active
+ .owner_is_human
+ .last_used_before_or_unused(cut_off_date)
+ .for_users(group.users)
+ end
+ end
+
+ def revoke_batch(access_tokens, dry_run)
+ # Capture a simplified set of attributes for logging and for
+ # determining when an error has led some records to not be
+ # updated
+ attrs = access_tokens.as_json(only: [:id, :user_id])
+
+ # Use `update_all` to bypass any validations which might
+ # prevent revocation. Manually specify updated_at.
+ affected_row_count = dry_run ? 0 : access_tokens.update_all(revoked: true, updated_at: @revocation_time)
+
+ message = {
+ dry_run: dry_run,
+ message: "Revoke token batch",
+ token_count: attrs.size,
+ updated_count: affected_row_count,
+ tokens: attrs,
+ group_full_path: group.full_path
+ }
+
+ # rubocop:disable Rails/Output
+ if dry_run
+ puts "Dry run complete. #{attrs.size} rows would be affected"
+ logger.info(message)
+ elsif affected_row_count.eql?(attrs.size)
+ puts "Finished. #{attrs.size} rows affected"
+ logger.info(message)
+ else
+ # :nocov:
+ puts "ERROR. #{affected_row_count} tokens deleted, #{attrs.size} tokens should have been deleted"
+ logger.warn(message)
+ # :nocov:
+ end
+ # rubocop:enable Rails/Output
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/closing_issue_extractor.rb b/lib/gitlab/closing_issue_extractor.rb
index 8e624215065..7104de2a3c3 100644
--- a/lib/gitlab/closing_issue_extractor.rb
+++ b/lib/gitlab/closing_issue_extractor.rb
@@ -17,7 +17,6 @@ module Gitlab
def closed_by_message(message)
return [] if message.nil?
- return [] unless @project.autoclose_referenced_issues
closing_statements = []
message.scan(ISSUE_CLOSING_REGEX) do
@@ -27,8 +26,9 @@ module Gitlab
@extractor.analyze(closing_statements.join(" "))
@extractor.issues.reject do |issue|
- # Don't extract issues from the project this project was forked from
- @extractor.project.forked_from?(issue.project)
+ @extractor.project.forked_from?(issue.project) ||
+ !issue.project.autoclose_referenced_issues ||
+ !issue.project.issues_enabled?
end
end
end
diff --git a/lib/gitlab/cluster/lifecycle_events.rb b/lib/gitlab/cluster/lifecycle_events.rb
index e423d1f17da..be08ada9d2f 100644
--- a/lib/gitlab/cluster/lifecycle_events.rb
+++ b/lib/gitlab/cluster/lifecycle_events.rb
@@ -4,6 +4,11 @@ require_relative '../utils' # Gitlab::Utils
module Gitlab
module Cluster
+ # We take advantage of the fact that the application is pre-loaded in the primary
+ # process. If it's a pre-fork server like Puma, this will be the Puma master process.
+ # Otherwise it is the worker itself such as for Sidekiq.
+ PRIMARY_PID = $$
+
#
# LifecycleEvents lets Rails initializers register application startup hooks
# that are sensitive to forking. For example, to defer the creation of
diff --git a/lib/gitlab/config/entry/composable_hash.rb b/lib/gitlab/config/entry/composable_hash.rb
index 9531b7e56fd..0b892fd4552 100644
--- a/lib/gitlab/config/entry/composable_hash.rb
+++ b/lib/gitlab/config/entry/composable_hash.rb
@@ -25,9 +25,9 @@ module Gitlab
entry_class_name = entry_class.name.demodulize.underscore
factory = ::Gitlab::Config::Entry::Factory.new(entry_class)
- .value(config || {})
+ .value(config.nil? ? {} : config)
.with(key: name, parent: self, description: "#{name} #{entry_class_name} definition") # rubocop:disable CodeReuse/ActiveRecord
- .metadata(name: name)
+ .metadata(composable_metadata.merge(name: name))
@entries[name] = factory.create!
end
@@ -38,9 +38,15 @@ module Gitlab
end
end
+ private
+
def composable_class(name, config)
opt(:composable_class)
end
+
+ def composable_metadata
+ {}
+ end
end
end
end
diff --git a/lib/gitlab/config/entry/validators.rb b/lib/gitlab/config/entry/validators.rb
index cc24ae837f3..337cfbc5287 100644
--- a/lib/gitlab/config/entry/validators.rb
+++ b/lib/gitlab/config/entry/validators.rb
@@ -304,6 +304,7 @@ module Gitlab
end
end
+ # This will be removed with the FF `ci_variables_refactoring_to_variable`.
class VariablesValidator < ActiveModel::EachValidator
include LegacyValidationHelpers
@@ -336,6 +337,18 @@ module Gitlab
end
end
+ class AlphanumericValidator < ActiveModel::EachValidator
+ def self.validate(value)
+ value.is_a?(String) || value.is_a?(Symbol) || value.is_a?(Integer)
+ end
+
+ def validate_each(record, attribute, value)
+ unless self.class.validate(value)
+ record.errors.add(attribute, 'must be an alphanumeric string')
+ end
+ end
+ end
+
class ExpressionValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
unless value.is_a?(String) && ::Gitlab::Ci::Pipeline::Expression::Statement.new(value).valid?
diff --git a/lib/gitlab/container_repository/tags/cache.rb b/lib/gitlab/container_repository/tags/cache.rb
index ff457fb9219..47a6e67a5a1 100644
--- a/lib/gitlab/container_repository/tags/cache.rb
+++ b/lib/gitlab/container_repository/tags/cache.rb
@@ -48,14 +48,14 @@ module Gitlab
::Gitlab::Redis::Cache.with do |redis|
# we use a pipeline instead of a MSET because each tag has
# a specific ttl
- redis.pipelined do
+ redis.pipelined do |pipeline|
cacheable_tags.each do |tag|
created_at = tag.created_at
# ttl is the max_ttl_in_seconds reduced by the number
# of seconds that the tag has already existed
ttl = max_ttl_in_seconds - (now - created_at).seconds
ttl = ttl.to_i
- redis.set(cache_key(tag), created_at.rfc3339, ex: ttl) if ttl > 0
+ pipeline.set(cache_key(tag), created_at.rfc3339, ex: ttl) if ttl > 0
end
end
end
diff --git a/lib/gitlab/data_builder/build.rb b/lib/gitlab/data_builder/build.rb
index 91e6fc11a53..4640f85bb0a 100644
--- a/lib/gitlab/data_builder/build.rb
+++ b/lib/gitlab/data_builder/build.rb
@@ -24,7 +24,7 @@ module Gitlab
# Leaving this way to have backward compatibility
build_id: build.id,
build_name: build.name,
- build_stage: build.stage,
+ build_stage: build.stage_name,
build_status: build.status,
build_created_at: build.created_at,
build_started_at: build.started_at,
diff --git a/lib/gitlab/data_builder/pipeline.rb b/lib/gitlab/data_builder/pipeline.rb
index 2c124b07006..320ebe5e80f 100644
--- a/lib/gitlab/data_builder/pipeline.rb
+++ b/lib/gitlab/data_builder/pipeline.rb
@@ -52,7 +52,8 @@ module Gitlab
runner: :tags,
job_artifacts_archive: [],
user: [],
- metadata: []
+ metadata: [],
+ ci_stage: []
}
}
)
@@ -110,7 +111,7 @@ module Gitlab
def build_hook_attrs(build)
{
id: build.id,
- stage: build.stage,
+ stage: build.stage_name,
name: build.name,
status: build.status,
created_at: build.created_at,
diff --git a/lib/gitlab/database.rb b/lib/gitlab/database.rb
index 8703365b678..dd84127459d 100644
--- a/lib/gitlab/database.rb
+++ b/lib/gitlab/database.rb
@@ -242,7 +242,8 @@ module Gitlab
# in such cases it is fine to ignore such connections
return unless db_config
- primary_model = self.database_base_models.fetch(db_config.name.to_sym)
+ db_config_name = db_config.name.delete_suffix(LoadBalancing::LoadBalancer::REPLICA_SUFFIX)
+ primary_model = self.database_base_models.fetch(db_config_name.to_sym)
self.schemas_to_base_models.select do |_, child_models|
child_models.any? do |child_model|
diff --git a/lib/gitlab/database/background_migration/batched_migration.rb b/lib/gitlab/database/background_migration/batched_migration.rb
index 6aed1eed994..45f52765d0f 100644
--- a/lib/gitlab/database/background_migration/batched_migration.rb
+++ b/lib/gitlab/database/background_migration/batched_migration.rb
@@ -8,6 +8,7 @@ module Gitlab
BATCH_CLASS_MODULE = "#{JOB_CLASS_MODULE}::BatchingStrategies"
MAXIMUM_FAILED_RATIO = 0.5
MINIMUM_JOBS = 50
+ FINISHED_PROGRESS_VALUE = 100
self.table_name = :batched_background_migrations
@@ -24,6 +25,7 @@ module Gitlab
scope :queue_order, -> { order(id: :asc) }
scope :queued, -> { with_statuses(:active, :paused) }
+ scope :ordered_by_created_at_desc, -> { order(created_at: :desc) }
# on_hold_until is a temporary runtime status which puts execution "on hold"
scope :executable, -> { with_status(:active).where('on_hold_until IS NULL OR on_hold_until < NOW()') }
@@ -57,11 +59,11 @@ module Gitlab
state :finalizing, value: 5
event :pause do
- transition any => :paused
+ transition [:active, :paused] => :paused
end
event :execute do
- transition any => :active
+ transition [:active, :paused, :failed] => :active
end
event :finish do
@@ -231,7 +233,15 @@ module Gitlab
"BatchedMigration[id: #{id}]"
end
+ # Computes an estimation of the progress of the migration in percents.
+ #
+ # Because `total_tuple_count` is an estimation of the tuples based on DB statistics
+ # when the migration is complete there can actually be more or less tuples that initially
+ # estimated as `total_tuple_count` so the progress may not show 100%. For that reason when
+ # we know migration completed successfully, we just return the 100 value
def progress
+ return FINISHED_PROGRESS_VALUE if finished?
+
return unless total_tuple_count.to_i > 0
100 * migrated_tuple_count / total_tuple_count
diff --git a/lib/gitlab/database/background_migration/health_status.rb b/lib/gitlab/database/background_migration/health_status.rb
index 9a283074b32..506d2996ad5 100644
--- a/lib/gitlab/database/background_migration/health_status.rb
+++ b/lib/gitlab/database/background_migration/health_status.rb
@@ -18,7 +18,7 @@ module Gitlab
indicator.new(migration.health_context).evaluate
rescue StandardError => e
Gitlab::ErrorTracking.track_exception(e, migration_id: migration.id,
- job_class_name: migration.job_class_name)
+ job_class_name: migration.job_class_name)
Signals::Unknown.new(indicator, reason: "unexpected error: #{e.message} (#{e.class})")
end
diff --git a/lib/gitlab/database/batch_average_counter.rb b/lib/gitlab/database/batch_average_counter.rb
new file mode 100644
index 00000000000..9cb1e34ab67
--- /dev/null
+++ b/lib/gitlab/database/batch_average_counter.rb
@@ -0,0 +1,103 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Database
+ class BatchAverageCounter
+ COLUMN_FALLBACK = 0
+ DEFAULT_BATCH_SIZE = 1_000
+ FALLBACK = -1
+ MAX_ALLOWED_LOOPS = 10_000
+ OFFSET_BY_ONE = 1
+ SLEEP_TIME_IN_SECONDS = 0.01 # 10 msec sleep
+
+ attr_reader :relation, :column
+
+ def initialize(relation, column)
+ @relation = relation
+ @column = wrap_column(relation, column)
+ end
+
+ def count(batch_size: nil)
+ raise 'BatchAverageCounter can not be run inside a transaction' if transaction_open?
+
+ batch_size = batch_size.presence || DEFAULT_BATCH_SIZE
+
+ start = column_start
+ finish = column_finish
+
+ total_sum = 0
+ total_records = 0
+
+ batch_start = start
+
+ while batch_start < finish
+ begin
+ batch_end = [batch_start + batch_size, finish].min
+ batch_relation = build_relation_batch(batch_start, batch_end)
+
+ # We use `sum` and `count` instead of `average` here to not run into an "average of averages"
+ # problem as batches will have different sizes, so we are essentially summing up the values for
+ # each batch separately, and then dividing that result on the total number of records.
+ batch_sum, batch_count = batch_relation.pick(column.sum, column.count)
+
+ total_sum += batch_sum.to_i
+ total_records += batch_count
+
+ batch_start = batch_end
+ rescue ActiveRecord::QueryCanceled => error # rubocop:disable Database/RescueQueryCanceled
+ # retry with a safe batch size & warmer cache
+ if batch_size >= 2 * DEFAULT_BATCH_SIZE
+ batch_size /= 2
+ else
+ log_canceled_batch_fetch(batch_start, batch_relation.to_sql, error)
+
+ return FALLBACK
+ end
+ end
+
+ sleep(SLEEP_TIME_IN_SECONDS)
+ end
+
+ return FALLBACK if total_records == 0
+
+ total_sum.to_f / total_records
+ end
+
+ private
+
+ def column_start
+ relation.unscope(:group, :having).minimum(column) || COLUMN_FALLBACK
+ end
+
+ def column_finish
+ (relation.unscope(:group, :having).maximum(column) || COLUMN_FALLBACK) + OFFSET_BY_ONE
+ end
+
+ def build_relation_batch(start, finish)
+ relation.where(column.between(start...finish))
+ end
+
+ def log_canceled_batch_fetch(batch_start, query, error)
+ Gitlab::AppJsonLogger
+ .error(
+ event: 'batch_count',
+ relation: relation.table_name,
+ operation: 'average',
+ start: batch_start,
+ query: query,
+ message: "Query has been canceled with message: #{error.message}"
+ )
+ end
+
+ def transaction_open?
+ relation.connection.transaction_open?
+ end
+
+ def wrap_column(relation, column)
+ return column if column.is_a?(Arel::Attributes::Attribute)
+
+ relation.arel_table[column]
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/batch_count.rb b/lib/gitlab/database/batch_count.rb
index 92a41bb36ee..7a064fb4005 100644
--- a/lib/gitlab/database/batch_count.rb
+++ b/lib/gitlab/database/batch_count.rb
@@ -35,6 +35,10 @@ module Gitlab
BatchCounter.new(relation, column: column).count(batch_size: batch_size, start: start, finish: finish)
end
+ def batch_count_with_timeout(relation, column = nil, batch_size: nil, start: nil, finish: nil, timeout: nil, partial_results: nil)
+ BatchCounter.new(relation, column: column).count_with_timeout(batch_size: batch_size, start: start, finish: finish, timeout: timeout, partial_results: partial_results)
+ end
+
def batch_distinct_count(relation, column = nil, batch_size: nil, start: nil, finish: nil)
BatchCounter.new(relation, column: column).count(mode: :distinct, batch_size: batch_size, start: start, finish: finish)
end
@@ -44,7 +48,7 @@ module Gitlab
end
def batch_average(relation, column, batch_size: nil, start: nil, finish: nil)
- BatchCounter.new(relation, column: nil, operation: :average, operation_args: [column]).count(batch_size: batch_size, start: start, finish: finish)
+ BatchAverageCounter.new(relation, column).count(batch_size: batch_size)
end
class << self
diff --git a/lib/gitlab/database/batch_counter.rb b/lib/gitlab/database/batch_counter.rb
index 522b598cd9d..abb62140503 100644
--- a/lib/gitlab/database/batch_counter.rb
+++ b/lib/gitlab/database/batch_counter.rb
@@ -6,7 +6,6 @@ module Gitlab
FALLBACK = -1
MIN_REQUIRED_BATCH_SIZE = 1_250
DEFAULT_SUM_BATCH_SIZE = 1_000
- DEFAULT_AVERAGE_BATCH_SIZE = 1_000
MAX_ALLOWED_LOOPS = 10_000
SLEEP_TIME_IN_SECONDS = 0.01 # 10 msec sleep
ALLOWED_MODES = [:itself, :distinct].freeze
@@ -27,12 +26,19 @@ module Gitlab
def unwanted_configuration?(finish, batch_size, start)
(@operation == :count && batch_size <= MIN_REQUIRED_BATCH_SIZE) ||
(@operation == :sum && batch_size < DEFAULT_SUM_BATCH_SIZE) ||
- (@operation == :average && batch_size < DEFAULT_AVERAGE_BATCH_SIZE) ||
(finish - start) / batch_size >= MAX_ALLOWED_LOOPS ||
start >= finish
end
def count(batch_size: nil, mode: :itself, start: nil, finish: nil)
+ result = count_with_timeout(batch_size: batch_size, mode: mode, start: start, finish: finish, timeout: nil)
+
+ return FALLBACK if result[:status] != :completed
+
+ result[:count]
+ end
+
+ def count_with_timeout(batch_size: nil, mode: :itself, start: nil, finish: nil, timeout: nil, partial_results: nil)
raise 'BatchCount can not be run inside a transaction' if transaction_open?
check_mode!(mode)
@@ -44,12 +50,20 @@ module Gitlab
finish = actual_finish(finish)
raise "Batch counting expects positive values only for #{@column}" if start < 0 || finish < 0
- return FALLBACK if unwanted_configuration?(finish, batch_size, start)
+ return { status: :bad_config } if unwanted_configuration?(finish, batch_size, start)
- results = nil
+ results = partial_results
batch_start = start
+ start_time = ::Gitlab::Metrics::System.monotonic_time.seconds
+
while batch_start < finish
+
+ # Timeout elapsed, return partial result so the caller can continue later
+ if timeout && ::Gitlab::Metrics::System.monotonic_time.seconds - start_time > timeout
+ return { status: :timeout, partial_results: results, continue_from: batch_start }
+ end
+
begin
batch_end = [batch_start + batch_size, finish].min
batch_relation = build_relation_batch(batch_start, batch_end, mode)
@@ -62,14 +76,14 @@ module Gitlab
batch_size /= 2
else
log_canceled_batch_fetch(batch_start, mode, batch_relation.to_sql, error)
- return FALLBACK
+ return { status: :cancelled }
end
end
sleep(SLEEP_TIME_IN_SECONDS)
end
- results
+ { status: :completed, count: results }
end
def transaction_open?
@@ -94,7 +108,6 @@ module Gitlab
def batch_size_for_mode_and_operation(mode, operation)
return DEFAULT_SUM_BATCH_SIZE if operation == :sum
- return DEFAULT_AVERAGE_BATCH_SIZE if operation == :average
mode == :distinct ? DEFAULT_DISTINCT_BATCH_SIZE : DEFAULT_BATCH_SIZE
end
@@ -132,10 +145,6 @@ module Gitlab
message: "Query has been canceled with message: #{error.message}"
)
end
-
- def not_group_by_query?
- !@relation.is_a?(ActiveRecord::Relation) || @relation.group_values.blank?
- end
end
end
end
diff --git a/lib/gitlab/database/gitlab_schemas.yml b/lib/gitlab/database/gitlab_schemas.yml
index d05eee7d6e6..5725d7a4503 100644
--- a/lib/gitlab/database/gitlab_schemas.yml
+++ b/lib/gitlab/database/gitlab_schemas.yml
@@ -91,6 +91,7 @@ ci_job_artifact_states: :gitlab_ci
ci_minutes_additional_packs: :gitlab_ci
ci_namespace_monthly_usages: :gitlab_ci
ci_namespace_mirrors: :gitlab_ci
+ci_partitions: :gitlab_ci
ci_pending_builds: :gitlab_ci
ci_pipeline_artifacts: :gitlab_ci
ci_pipeline_chat_data: :gitlab_ci
@@ -182,6 +183,7 @@ design_management_versions: :gitlab_main
design_user_mentions: :gitlab_main
detached_partitions: :gitlab_shared
diff_note_positions: :gitlab_main
+dora_configurations: :gitlab_main
dora_daily_metrics: :gitlab_main
draft_notes: :gitlab_main
elastic_index_settings: :gitlab_main
@@ -228,6 +230,7 @@ geo_repository_deleted_events: :gitlab_main
geo_repository_renamed_events: :gitlab_main
geo_repository_updated_events: :gitlab_main
geo_reset_checksum_events: :gitlab_main
+ghost_user_migrations: :gitlab_main
gitlab_subscription_histories: :gitlab_main
gitlab_subscriptions: :gitlab_main
gpg_keys: :gitlab_main
@@ -315,6 +318,7 @@ merge_request_diff_details: :gitlab_main
merge_request_diff_files: :gitlab_main
merge_request_diffs: :gitlab_main
merge_request_metrics: :gitlab_main
+merge_request_predictions: :gitlab_main
merge_request_reviewers: :gitlab_main
merge_requests_closing_issues: :gitlab_main
merge_requests: :gitlab_main
@@ -380,6 +384,7 @@ packages_events: :gitlab_main
packages_helm_file_metadata: :gitlab_main
packages_maven_metadata: :gitlab_main
packages_npm_metadata: :gitlab_main
+packages_rpm_metadata: :gitlab_main
packages_nuget_dependency_link_metadata: :gitlab_main
packages_nuget_metadata: :gitlab_main
packages_package_file_build_infos: :gitlab_main
@@ -399,6 +404,7 @@ plans: :gitlab_main
pool_repositories: :gitlab_main
postgres_async_indexes: :gitlab_shared
postgres_autovacuum_activity: :gitlab_shared
+postgres_constraints: :gitlab_shared
postgres_foreign_keys: :gitlab_shared
postgres_index_bloat_estimates: :gitlab_shared
postgres_indexes: :gitlab_shared
@@ -479,6 +485,7 @@ sbom_components: :gitlab_main
sbom_occurrences: :gitlab_main
sbom_component_versions: :gitlab_main
sbom_sources: :gitlab_main
+sbom_vulnerable_component_versions: :gitlab_main
schema_migrations: :gitlab_internal
scim_identities: :gitlab_main
scim_oauth_access_tokens: :gitlab_main
@@ -549,6 +556,7 @@ user_statuses: :gitlab_main
user_synced_attributes_metadata: :gitlab_main
verification_codes: :gitlab_main
vulnerabilities: :gitlab_main
+vulnerability_advisories: :gitlab_main
vulnerability_exports: :gitlab_main
vulnerability_external_issue_links: :gitlab_main
vulnerability_feedback: :gitlab_main
diff --git a/lib/gitlab/database/lock_writes_manager.rb b/lib/gitlab/database/lock_writes_manager.rb
index cd483d616bb..fe75cd763b4 100644
--- a/lib/gitlab/database/lock_writes_manager.rb
+++ b/lib/gitlab/database/lock_writes_manager.rb
@@ -5,42 +5,63 @@ module Gitlab
class LockWritesManager
TRIGGER_FUNCTION_NAME = 'gitlab_schema_prevent_write'
- def initialize(table_name:, connection:, database_name:, logger: nil)
+ def initialize(table_name:, connection:, database_name:, logger: nil, dry_run: false)
@table_name = table_name
@connection = connection
@database_name = database_name
@logger = logger
+ @dry_run = dry_run
+ end
+
+ def table_locked_for_writes?(table_name)
+ query = <<~SQL
+ SELECT COUNT(*) from information_schema.triggers
+ WHERE event_object_table = '#{table_name}'
+ AND trigger_name = '#{write_trigger_name(table_name)}'
+ SQL
+
+ connection.select_value(query) == 3
end
def lock_writes
+ if table_locked_for_writes?(table_name)
+ logger&.info "Skipping lock_writes, because #{table_name} is already locked for writes"
+ return
+ end
+
logger&.info "Database: '#{database_name}', Table: '#{table_name}': Lock Writes".color(:yellow)
- sql = <<-SQL
- DROP TRIGGER IF EXISTS #{write_trigger_name(table_name)} ON #{table_name};
+ sql_statement = <<~SQL
CREATE TRIGGER #{write_trigger_name(table_name)}
BEFORE INSERT OR UPDATE OR DELETE OR TRUNCATE
ON #{table_name}
FOR EACH STATEMENT EXECUTE FUNCTION #{TRIGGER_FUNCTION_NAME}();
SQL
- with_retries(connection) do
- connection.execute(sql)
- end
+ execute_sql_statement(sql_statement)
end
def unlock_writes
logger&.info "Database: '#{database_name}', Table: '#{table_name}': Allow Writes".color(:green)
- sql = <<-SQL
- DROP TRIGGER IF EXISTS #{write_trigger_name(table_name)} ON #{table_name}
+ sql_statement = <<~SQL
+ DROP TRIGGER IF EXISTS #{write_trigger_name(table_name)} ON #{table_name};
SQL
- with_retries(connection) do
- connection.execute(sql)
- end
+ execute_sql_statement(sql_statement)
end
private
- attr_reader :table_name, :connection, :database_name, :logger
+ attr_reader :table_name, :connection, :database_name, :logger, :dry_run
+
+ def execute_sql_statement(sql)
+ if dry_run
+ logger&.info sql
+ else
+ with_retries(connection) do
+ connection.execute(sql)
+ end
+ end
+ end
def with_retries(connection, &block)
with_statement_timeout_retries do
diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb
index db39524f4f6..e574422ce11 100644
--- a/lib/gitlab/database/migration_helpers.rb
+++ b/lib/gitlab/database/migration_helpers.rb
@@ -936,13 +936,14 @@ module Gitlab
def revert_backfill_conversion_of_integer_to_bigint(table, columns, primary_key: :id)
columns = Array.wrap(columns)
- conditions = ActiveRecord::Base.sanitize_sql([
- 'job_class_name = :job_class_name AND table_name = :table_name AND column_name = :column_name AND job_arguments = :job_arguments',
- job_class_name: 'CopyColumnUsingBackgroundMigrationJob',
- table_name: table,
- column_name: primary_key,
- job_arguments: [columns, columns.map { |column| convert_to_bigint_column(column) }].to_json
- ])
+ conditions = ActiveRecord::Base.sanitize_sql(
+ [
+ 'job_class_name = :job_class_name AND table_name = :table_name AND column_name = :column_name AND job_arguments = :job_arguments',
+ job_class_name: 'CopyColumnUsingBackgroundMigrationJob',
+ table_name: table,
+ column_name: primary_key,
+ job_arguments: [columns, columns.map { |column| convert_to_bigint_column(column) }].to_json
+ ])
execute("DELETE FROM batched_background_migrations WHERE #{conditions}")
end
diff --git a/lib/gitlab/database/migrations/base_background_runner.rb b/lib/gitlab/database/migrations/base_background_runner.rb
index a9440cafd30..76982a9da9b 100644
--- a/lib/gitlab/database/migrations/base_background_runner.rb
+++ b/lib/gitlab/database/migrations/base_background_runner.rb
@@ -40,7 +40,7 @@ module Gitlab
instrumentation = Instrumentation.new(result_dir: per_background_migration_result_dir)
batch_names = (1..).each.lazy.map { |i| "batch_#{i}" }
- jobs.shuffle.each do |j|
+ jobs.each do |j|
break if run_until <= Time.current
instrumentation.observe(version: nil,
diff --git a/lib/gitlab/database/migrations/test_background_runner.rb b/lib/gitlab/database/migrations/test_background_runner.rb
index f7713237b38..6da2e098d43 100644
--- a/lib/gitlab/database/migrations/test_background_runner.rb
+++ b/lib/gitlab/database/migrations/test_background_runner.rb
@@ -15,6 +15,7 @@ module Gitlab
def jobs_by_migration_name
traditional_background_migrations.group_by { |j| class_name_for_job(j) }
+ .transform_values(&:shuffle)
end
private
diff --git a/lib/gitlab/database/migrations/test_batched_background_runner.rb b/lib/gitlab/database/migrations/test_batched_background_runner.rb
index f38d847b0e8..c27ae6a2c5d 100644
--- a/lib/gitlab/database/migrations/test_batched_background_runner.rb
+++ b/lib/gitlab/database/migrations/test_batched_background_runner.rb
@@ -4,6 +4,7 @@ module Gitlab
module Database
module Migrations
class TestBatchedBackgroundRunner < BaseBackgroundRunner
+ include Gitlab::Database::DynamicModelHelpers
attr_reader :connection
def initialize(result_dir:, connection:)
@@ -18,31 +19,81 @@ module Gitlab
.to_h do |migration|
batching_strategy = migration.batch_class.new(connection: connection)
- all_migration_jobs = []
+ smallest_batch_start = migration.next_min_value
- min_value = migration.next_min_value
+ table_max_value = define_batchable_model(migration.table_name, connection: connection)
+ .maximum(migration.column_name)
- while (next_bounds = batching_strategy.next_batch(
- migration.table_name,
- migration.column_name,
- batch_min_value: min_value,
- batch_size: migration.batch_size,
- job_arguments: migration.job_arguments
- ))
+ largest_batch_start = table_max_value - migration.batch_size
+
+ # variance is the portion of the batch range that we shrink between variance * 0 and variance * 1
+ # to pick actual batches to sample.
+ variance = largest_batch_start - smallest_batch_start
+
+ batch_starts = uniform_fractions
+ .lazy # frac varies from 0 to 1, values in smallest_batch_start..largest_batch_start
+ .map { |frac| (variance * frac).to_i + smallest_batch_start }
+
+ # Track previously run batches so that we stop sampling if a new batch would intersect an older one
+ completed_batches = []
+
+ jobs_to_sample = batch_starts
+ # Stop sampling if a batch would intersect a previous batch
+ .take_while { |start| completed_batches.none? { |batch| batch.cover?(start) } }
+ .map do |batch_start|
+ next_bounds = batching_strategy.next_batch(
+ migration.table_name,
+ migration.column_name,
+ batch_min_value: batch_start,
+ batch_size: migration.batch_size,
+ job_arguments: migration.job_arguments
+ )
batch_min, batch_max = next_bounds
- all_migration_jobs << migration.create_batched_job!(batch_min, batch_max)
- min_value = batch_max + 1
+ job = migration.create_batched_job!(batch_min, batch_max)
+
+ completed_batches << (batch_min..batch_max)
+
+ job
end
- [migration.job_class_name, all_migration_jobs]
+ [migration.job_class_name, jobs_to_sample]
end
end
def run_job(job)
Gitlab::Database::BackgroundMigration::BatchedMigrationWrapper.new(connection: connection).perform(job)
end
+
+ def uniform_fractions
+ Enumerator.new do |y|
+ # Generates equally distributed fractions between 0 and 1, with increasing detail as more are pulled from
+ # the enumerator.
+ # 0, 1 (special case)
+ # 1/2
+ # 1/4, 3/4
+ # 1/8, 3/8, 5/8, 7/8
+ # etc.
+ # The pattern here is at each outer loop, the denominator multiplies by 2, and at each inner loop,
+ # the numerator counts up all odd numbers 1 <= n < denominator.
+ y << 0
+ y << 1
+
+ # denominators are each increasing power of 2
+ denominators = (1..).lazy.map { |exponent| 2**exponent }
+
+ denominators.each do |denominator|
+ # Numerators at the current step are all odd numbers between 1 and the denominator
+ numerators = (1..denominator).step(2)
+
+ numerators.each do |numerator|
+ next_frac = numerator.fdiv(denominator)
+ y << next_frac
+ end
+ end
+ end
+ end
end
end
end
diff --git a/lib/gitlab/database/partitioning.rb b/lib/gitlab/database/partitioning.rb
index 92825d41599..6314aff9914 100644
--- a/lib/gitlab/database/partitioning.rb
+++ b/lib/gitlab/database/partitioning.rb
@@ -33,6 +33,18 @@ module Gitlab
PartitionManager.new(model).sync_partitions
end
+ unless only_on
+ models_to_sync.each do |model|
+ next if model < ::Gitlab::Database::SharedModel && !(model < TableWithoutModel)
+
+ Gitlab::Database::EachDatabase.each_database_connection do |connection, connection_name|
+ if connection_name != model.connection_db_config.name
+ PartitionManager.new(model, connection: connection).sync_partitions
+ end
+ end
+ end
+ end
+
Gitlab::AppLogger.info(message: 'Finished sync of dynamic postgres partitions')
end
diff --git a/lib/gitlab/database/partitioning/convert_table_to_first_list_partition.rb b/lib/gitlab/database/partitioning/convert_table_to_first_list_partition.rb
new file mode 100644
index 00000000000..f45cf02ec9b
--- /dev/null
+++ b/lib/gitlab/database/partitioning/convert_table_to_first_list_partition.rb
@@ -0,0 +1,214 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Database
+ module Partitioning
+ class ConvertTableToFirstListPartition
+ UnableToPartition = Class.new(StandardError)
+
+ include Gitlab::Database::MigrationHelpers
+
+ SQL_STATEMENT_SEPARATOR = ";\n\n"
+
+ attr_reader :partitioning_column, :table_name, :parent_table_name, :zero_partition_value
+
+ def initialize(migration_context:, table_name:, parent_table_name:, partitioning_column:, zero_partition_value:)
+ @migration_context = migration_context
+ @connection = migration_context.connection
+ @table_name = table_name
+ @parent_table_name = parent_table_name
+ @partitioning_column = partitioning_column
+ @zero_partition_value = zero_partition_value
+ end
+
+ def prepare_for_partitioning
+ assert_existing_constraints_partitionable
+
+ add_partitioning_check_constraint
+ end
+
+ def revert_preparation_for_partitioning
+ migration_context.remove_check_constraint(table_name, partitioning_constraint.name)
+ end
+
+ def partition
+ assert_existing_constraints_partitionable
+ assert_partitioning_constraint_present
+ create_parent_table
+ attach_foreign_keys_to_parent
+
+ migration_context.with_lock_retries(raise_on_exhaustion: true) do
+ migration_context.execute(sql_to_convert_table)
+ end
+ end
+
+ def revert_partitioning
+ migration_context.with_lock_retries(raise_on_exhaustion: true) do
+ migration_context.execute(<<~SQL)
+ ALTER TABLE #{connection.quote_table_name(parent_table_name)}
+ DETACH PARTITION #{connection.quote_table_name(table_name)};
+ SQL
+
+ alter_sequences_sql = alter_sequence_statements(old_table: parent_table_name, new_table: table_name)
+ .join(SQL_STATEMENT_SEPARATOR)
+
+ migration_context.execute(alter_sequences_sql)
+
+ # This takes locks for all the foreign keys that the parent table had.
+ # However, those same locks were taken while detaching the partition, and we can't avoid that.
+ # If we dropped the foreign key before detaching the partition to avoid this locking,
+ # the drop would cascade to the child partitions and drop their foreign keys as well
+ migration_context.drop_table(parent_table_name)
+ end
+
+ add_partitioning_check_constraint
+ end
+
+ private
+
+ attr_reader :connection, :migration_context
+
+ delegate :quote_table_name, :quote_column_name, to: :connection
+
+ def sql_to_convert_table
+ # The critical statement here is the attach_table_to_parent statement.
+ # The following statements could be run in a later transaction,
+ # but they acquire the same locks so it's much faster to incude them
+ # here.
+ [
+ attach_table_to_parent_statement,
+ alter_sequence_statements(old_table: table_name, new_table: parent_table_name),
+ remove_constraint_statement
+ ].flatten.join(SQL_STATEMENT_SEPARATOR)
+ end
+
+ def table_identifier
+ "#{connection.current_schema}.#{table_name}"
+ end
+
+ def assert_existing_constraints_partitionable
+ violating_constraints = Gitlab::Database::PostgresConstraint
+ .by_table_identifier(table_identifier)
+ .primary_or_unique_constraints
+ .not_including_column(partitioning_column)
+ .to_a
+
+ return if violating_constraints.empty?
+
+ violation_messages = violating_constraints.map { |c| "#{c.name} on (#{c.column_names.join(', ')})" }
+
+ raise UnableToPartition, <<~MSG
+ Constraints on #{table_name} are incompatible with partitioning on #{partitioning_column}
+
+ All primary key and unique constraints must include the partitioning column.
+ Violations:
+ #{violation_messages.join("\n")}
+ MSG
+ end
+
+ def partitioning_constraint
+ constraints_on_column = Gitlab::Database::PostgresConstraint
+ .by_table_identifier(table_identifier)
+ .check_constraints
+ .valid
+ .including_column(partitioning_column)
+
+ constraints_on_column.to_a.find do |constraint|
+ constraint.definition == "CHECK ((#{partitioning_column} = #{zero_partition_value}))"
+ end
+ end
+
+ def assert_partitioning_constraint_present
+ return if partitioning_constraint
+
+ raise UnableToPartition, <<~MSG
+ Table #{table_name} is not ready for partitioning.
+ Before partitioning, a check constraint must enforce that (#{partitioning_column} = #{zero_partition_value})
+ MSG
+ end
+
+ def add_partitioning_check_constraint
+ return if partitioning_constraint.present?
+
+ check_body = "#{partitioning_column} = #{connection.quote(zero_partition_value)}"
+ # Any constraint name would work. The constraint is found based on its definition before partitioning
+ migration_context.add_check_constraint(table_name, check_body, 'partitioning_constraint')
+
+ raise UnableToPartition, 'Error adding partitioning constraint' unless partitioning_constraint.present?
+ end
+
+ def create_parent_table
+ migration_context.execute(<<~SQL)
+ CREATE TABLE IF NOT EXISTS #{quote_table_name(parent_table_name)} (
+ LIKE #{quote_table_name(table_name)} INCLUDING ALL
+ ) PARTITION BY LIST(#{quote_column_name(partitioning_column)})
+ SQL
+ end
+
+ def attach_foreign_keys_to_parent
+ migration_context.foreign_keys(table_name).each do |fk|
+ # At this point no other connection knows about the parent table.
+ # Thus the only contended lock in the following transaction is on fk.to_table.
+ # So a deadlock is impossible.
+
+ # If we're rerunning this migration after a failure to acquire a lock, the foreign key might already exist.
+ # Don't try to recreate it in that case
+ if migration_context.foreign_keys(parent_table_name)
+ .any? { |p_fk| p_fk.options[:name] == fk.options[:name] }
+ next
+ end
+
+ migration_context.with_lock_retries(raise_on_exhaustion: true) do
+ migration_context.add_foreign_key(parent_table_name, fk.to_table, **fk.options)
+ end
+ end
+ end
+
+ def attach_table_to_parent_statement
+ <<~SQL
+ ALTER TABLE #{quote_table_name(parent_table_name)}
+ ATTACH PARTITION #{table_name}
+ FOR VALUES IN (#{zero_partition_value})
+ SQL
+ end
+
+ def alter_sequence_statements(old_table:, new_table:)
+ sequences_owned_by(old_table).map do |seq_info|
+ seq_name, column_name = seq_info.values_at(:name, :column_name)
+ <<~SQL.chomp
+ ALTER SEQUENCE #{quote_table_name(seq_name)} OWNED BY #{quote_table_name(new_table)}.#{quote_column_name(column_name)}
+ SQL
+ end
+ end
+
+ def remove_constraint_statement
+ <<~SQL
+ ALTER TABLE #{quote_table_name(parent_table_name)}
+ DROP CONSTRAINT #{quote_table_name(partitioning_constraint.name)}
+ SQL
+ end
+
+ # TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/373887
+ def sequences_owned_by(table_name)
+ sequence_data = connection.exec_query(<<~SQL, nil, [table_name])
+ SELECT seq_pg_class.relname AS seq_name,
+ dep_pg_class.relname AS table_name,
+ pg_attribute.attname AS col_name
+ FROM pg_class seq_pg_class
+ INNER JOIN pg_depend ON seq_pg_class.oid = pg_depend.objid
+ INNER JOIN pg_class dep_pg_class ON pg_depend.refobjid = dep_pg_class.oid
+ INNER JOIN pg_attribute ON dep_pg_class.oid = pg_attribute.attrelid
+ AND pg_depend.refobjsubid = pg_attribute.attnum
+ WHERE seq_pg_class.relkind = 'S'
+ AND dep_pg_class.relname = $1
+ SQL
+
+ sequence_data.map do |seq_info|
+ name, column_name = seq_info.values_at('seq_name', 'col_name')
+ { name: name, column_name: column_name }
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/partitioning/partition_manager.rb b/lib/gitlab/database/partitioning/partition_manager.rb
index aac91eaadb1..55ca9ff8645 100644
--- a/lib/gitlab/database/partitioning/partition_manager.rb
+++ b/lib/gitlab/database/partitioning/partition_manager.rb
@@ -10,12 +10,15 @@ module Gitlab
MANAGEMENT_LEASE_KEY = 'database_partition_management_%s'
RETAIN_DETACHED_PARTITIONS_FOR = 1.week
- def initialize(model)
+ def initialize(model, connection: nil)
@model = model
- @connection_name = model.connection.pool.db_config.name
+ @connection = connection || model.connection
+ @connection_name = @connection.pool.db_config.name
end
def sync_partitions
+ return skip_synching_partitions unless table_partitioned?
+
Gitlab::AppLogger.info(
message: "Checking state of dynamic postgres partitions",
table_name: model.table_name,
@@ -43,9 +46,7 @@ module Gitlab
private
- attr_reader :model
-
- delegate :connection, to: :model
+ attr_reader :model, :connection
def missing_partitions
return [] unless connection.table_exists?(model.table_name)
@@ -129,6 +130,20 @@ module Gitlab
connection: connection
).run(&block)
end
+
+ def table_partitioned?
+ Gitlab::Database::SharedModel.using_connection(connection) do
+ Gitlab::Database::PostgresPartitionedTable.find_by_name_in_current_schema(model.table_name).present?
+ end
+ end
+
+ def skip_synching_partitions
+ Gitlab::AppLogger.warn(
+ message: "Skipping synching partitions",
+ table_name: model.table_name,
+ connection_name: @connection_name
+ )
+ end
end
end
end
diff --git a/lib/gitlab/database/partitioning/single_numeric_list_partition.rb b/lib/gitlab/database/partitioning/single_numeric_list_partition.rb
index 23ac73a0e53..4e38eea963b 100644
--- a/lib/gitlab/database/partitioning/single_numeric_list_partition.rb
+++ b/lib/gitlab/database/partitioning/single_numeric_list_partition.rb
@@ -8,7 +8,7 @@ module Gitlab
def self.from_sql(table, partition_name, definition)
# A list partition can support multiple values, but we only support a single number
- matches = definition.match(/FOR VALUES IN \('(?<value>\d+)'\)/)
+ matches = definition.match(/FOR VALUES IN \('?(?<value>\d+)'?\)/)
raise ArgumentError, 'Unknown partition definition' unless matches
@@ -29,17 +29,21 @@ module Gitlab
@partition_name || "#{table}_#{value}"
end
+ def data_size
+ execute("SELECT pg_table_size(#{quote(full_partition_name)})").first['pg_table_size']
+ end
+
def to_sql
<<~SQL
CREATE TABLE IF NOT EXISTS #{fully_qualified_partition}
- PARTITION OF #{conn.quote_table_name(table)}
- FOR VALUES IN (#{conn.quote(value)})
+ PARTITION OF #{quote_table_name(table)}
+ FOR VALUES IN (#{quote(value)})
SQL
end
def to_detach_sql
<<~SQL
- ALTER TABLE #{conn.quote_table_name(table)}
+ ALTER TABLE #{quote_table_name(table)}
DETACH PARTITION #{fully_qualified_partition}
SQL
end
@@ -63,8 +67,14 @@ module Gitlab
private
+ delegate :execute, :quote, :quote_table_name, to: :conn, private: true
+
+ def full_partition_name
+ "%s.%s" % [Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA, partition_name]
+ end
+
def fully_qualified_partition
- "%s.%s" % [conn.quote_table_name(Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA), conn.quote_table_name(partition_name)]
+ quote_table_name(full_partition_name)
end
def conn
diff --git a/lib/gitlab/database/partitioning/sliding_list_strategy.rb b/lib/gitlab/database/partitioning/sliding_list_strategy.rb
index 4b5349f0327..5bb34a86d43 100644
--- a/lib/gitlab/database/partitioning/sliding_list_strategy.rb
+++ b/lib/gitlab/database/partitioning/sliding_list_strategy.rb
@@ -14,7 +14,7 @@ module Gitlab
@next_partition_if = next_partition_if
@detach_partition_if = detach_partition_if
- ensure_partitioning_column_ignored!
+ ensure_partitioning_column_ignored_or_readonly!
end
def current_partitions
@@ -26,7 +26,7 @@ module Gitlab
def missing_partitions
if no_partitions_exist?
[initial_partition]
- elsif next_partition_if.call(active_partition.value)
+ elsif next_partition_if.call(active_partition)
[next_partition]
else
[]
@@ -44,7 +44,7 @@ module Gitlab
def extra_partitions
possibly_extra = current_partitions[0...-1] # Never consider the most recent partition
- extra = possibly_extra.take_while { |p| detach_partition_if.call(p.value) }
+ extra = possibly_extra.take_while { |p| detach_partition_if.call(p) }
default_value = current_default_value
if extra.any? { |p| p.value == default_value }
@@ -128,12 +128,17 @@ module Gitlab
Integer(value)
end
- def ensure_partitioning_column_ignored!
- unless model.ignored_columns.include?(partitioning_key.to_s)
- raise "Add #{partitioning_key} to #{model.name}.ignored_columns to use it with SlidingListStrategy"
+ def ensure_partitioning_column_ignored_or_readonly!
+ unless key_ignored_or_readonly?
+ raise "Add #{partitioning_key} to #{model.name}.ignored_columns or " \
+ "mark it as readonly to use it with SlidingListStrategy"
end
end
+ def key_ignored_or_readonly?
+ model.ignored_columns.include?(partitioning_key.to_s) || model.readonly_attribute?(partitioning_key.to_s)
+ end
+
def with_lock_retries(&block)
Gitlab::Database::WithLockRetries.new(
klass: self.class,
diff --git a/lib/gitlab/database/partitioning_migration_helpers/index_helpers.rb b/lib/gitlab/database/partitioning_migration_helpers/index_helpers.rb
index c9a3b5caf79..15b542cf089 100644
--- a/lib/gitlab/database/partitioning_migration_helpers/index_helpers.rb
+++ b/lib/gitlab/database/partitioning_migration_helpers/index_helpers.rb
@@ -77,8 +77,42 @@ module Gitlab
end
end
+ # Finds duplicate indexes for a given schema and table. This finds
+ # indexes where the index definition is identical but the names are
+ # different. Returns an array of arrays containing duplicate index name
+ # pairs.
+ #
+ # Example:
+ #
+ # find_duplicate_indexes('table_name_goes_here')
+ def find_duplicate_indexes(table_name, schema_name: connection.current_schema)
+ find_indexes(table_name, schema_name: schema_name)
+ .group_by { |r| r['index_id'] }
+ .select { |_, v| v.size > 1 }
+ .map { |_, indexes| indexes.map { |index| index['index_name'] } }
+ end
+
private
+ def find_indexes(table_name, schema_name: connection.current_schema)
+ indexes = connection.select_all(<<~SQL, 'SQL', [schema_name, table_name])
+ SELECT n.nspname AS schema_name,
+ c.relname AS table_name,
+ i.relname AS index_name,
+ regexp_replace(pg_get_indexdef(i.oid), 'INDEX .*? USING', '_') AS index_id
+ FROM pg_index x
+ JOIN pg_class c ON c.oid = x.indrelid
+ JOIN pg_class i ON i.oid = x.indexrelid
+ LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
+ WHERE (c.relkind = ANY (ARRAY['r'::"char", 'm'::"char", 'p'::"char"]))
+ AND (i.relkind = ANY (ARRAY['i'::"char", 'I'::"char"]))
+ AND n.nspname = $1
+ AND c.relname = $2;
+ SQL
+
+ indexes.to_a
+ end
+
def find_partitioned_table(table_name)
partitioned_table = Gitlab::Database::PostgresPartitionedTable.find_by_name_in_current_schema(table_name)
diff --git a/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb b/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb
index a541ecf5316..695a5d7ec77 100644
--- a/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb
+++ b/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb
@@ -251,6 +251,54 @@ module Gitlab
create_sync_trigger(source_table_name, trigger_name, function_name)
end
+ def prepare_constraint_for_list_partitioning(table_name:, partitioning_column:, parent_table_name:, initial_partitioning_value:)
+ validate_not_in_transaction!(:prepare_constraint_for_list_partitioning)
+
+ Gitlab::Database::Partitioning::ConvertTableToFirstListPartition
+ .new(migration_context: self,
+ table_name: table_name,
+ parent_table_name: parent_table_name,
+ partitioning_column: partitioning_column,
+ zero_partition_value: initial_partitioning_value
+ ).prepare_for_partitioning
+ end
+
+ def revert_preparing_constraint_for_list_partitioning(table_name:, partitioning_column:, parent_table_name:, initial_partitioning_value:)
+ validate_not_in_transaction!(:revert_preparing_constraint_for_list_partitioning)
+
+ Gitlab::Database::Partitioning::ConvertTableToFirstListPartition
+ .new(migration_context: self,
+ table_name: table_name,
+ parent_table_name: parent_table_name,
+ partitioning_column: partitioning_column,
+ zero_partition_value: initial_partitioning_value
+ ).revert_preparation_for_partitioning
+ end
+
+ def convert_table_to_first_list_partition(table_name:, partitioning_column:, parent_table_name:, initial_partitioning_value:)
+ validate_not_in_transaction!(:convert_table_to_first_list_partition)
+
+ Gitlab::Database::Partitioning::ConvertTableToFirstListPartition
+ .new(migration_context: self,
+ table_name: table_name,
+ parent_table_name: parent_table_name,
+ partitioning_column: partitioning_column,
+ zero_partition_value: initial_partitioning_value
+ ).partition
+ end
+
+ def revert_converting_table_to_first_list_partition(table_name:, partitioning_column:, parent_table_name:, initial_partitioning_value:)
+ validate_not_in_transaction!(:revert_converting_table_to_first_list_partition)
+
+ Gitlab::Database::Partitioning::ConvertTableToFirstListPartition
+ .new(migration_context: self,
+ table_name: table_name,
+ parent_table_name: parent_table_name,
+ partitioning_column: partitioning_column,
+ zero_partition_value: initial_partitioning_value
+ ).revert_partitioning
+ end
+
private
def assert_table_is_allowed(table_name)
diff --git a/lib/gitlab/database/postgres_constraint.rb b/lib/gitlab/database/postgres_constraint.rb
new file mode 100644
index 00000000000..fa590914332
--- /dev/null
+++ b/lib/gitlab/database/postgres_constraint.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Database
+ # Backed by the postgres_constraints view
+ class PostgresConstraint < SharedModel
+ IDENTIFIER_REGEX = /^\w+\.\w+$/.freeze
+ self.primary_key = :oid
+
+ scope :check_constraints, -> { where(constraint_type: 'c') }
+ scope :primary_key_constraints, -> { where(constraint_type: 'p') }
+ scope :unique_constraints, -> { where(constraint_type: 'u') }
+ scope :primary_or_unique_constraints, -> { where(constraint_type: %w[u p]) }
+
+ scope :including_column, ->(column) { where("? = ANY(column_names)", column) }
+ scope :not_including_column, ->(column) { where.not("? = ANY(column_names)", column) }
+
+ scope :valid, -> { where(constraint_valid: true) }
+
+ scope :by_table_identifier, ->(identifier) do
+ unless identifier =~ IDENTIFIER_REGEX
+ raise ArgumentError, "Table name is not fully qualified with a schema: #{identifier}"
+ end
+
+ where(table_identifier: identifier)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/query_analyzers/ci/partitioning_analyzer.rb b/lib/gitlab/database/query_analyzers/ci/partitioning_analyzer.rb
new file mode 100644
index 00000000000..c2d5dfc1a15
--- /dev/null
+++ b/lib/gitlab/database/query_analyzers/ci/partitioning_analyzer.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Database
+ module QueryAnalyzers
+ module Ci
+ # The purpose of this analyzer is to detect queries not going through a partitioning routing table
+ class PartitioningAnalyzer < Database::QueryAnalyzers::Base
+ RoutingTableNotUsedError = Class.new(QueryAnalyzerError)
+
+ ENABLED_TABLES = %w[
+ ci_builds_metadata
+ ].freeze
+
+ class << self
+ def enabled?
+ ::Feature::FlipperFeature.table_exists? &&
+ ::Feature.enabled?(:ci_partitioning_analyze_queries, type: :ops)
+ end
+
+ def analyze(parsed)
+ analyze_legacy_tables_usage(parsed)
+ end
+
+ private
+
+ def analyze_legacy_tables_usage(parsed)
+ detected = ENABLED_TABLES & (parsed.pg.dml_tables + parsed.pg.select_tables)
+
+ return if detected.none?
+
+ ::Gitlab::ErrorTracking.track_and_raise_for_dev_exception(
+ RoutingTableNotUsedError.new("Detected non-partitioned table use #{detected.inspect}: #{parsed.sql}")
+ )
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/query_analyzers/gitlab_schemas_metrics.rb b/lib/gitlab/database/query_analyzers/gitlab_schemas_metrics.rb
index 06e2b114c91..b4b9161f0c2 100644
--- a/lib/gitlab/database/query_analyzers/gitlab_schemas_metrics.rb
+++ b/lib/gitlab/database/query_analyzers/gitlab_schemas_metrics.rb
@@ -14,7 +14,7 @@ module Gitlab
class << self
def enabled?
::Feature::FlipperFeature.table_exists? &&
- Feature.enabled?(:query_analyzer_gitlab_schema_metrics)
+ Feature.enabled?(:query_analyzer_gitlab_schema_metrics, type: :ops)
end
def analyze(parsed)
diff --git a/lib/gitlab/database/query_analyzers/prevent_cross_database_modification.rb b/lib/gitlab/database/query_analyzers/prevent_cross_database_modification.rb
index e0cb803b872..3b1751c863d 100644
--- a/lib/gitlab/database/query_analyzers/prevent_cross_database_modification.rb
+++ b/lib/gitlab/database/query_analyzers/prevent_cross_database_modification.rb
@@ -33,7 +33,7 @@ module Gitlab
def self.enabled?
::Feature::FlipperFeature.table_exists? &&
- Feature.enabled?(:detect_cross_database_modification)
+ Feature.enabled?(:detect_cross_database_modification, type: :ops)
end
def self.requires_tracking?(parsed)
diff --git a/lib/gitlab/database/reflection.rb b/lib/gitlab/database/reflection.rb
index 3ea7277571f..33c965cb150 100644
--- a/lib/gitlab/database/reflection.rb
+++ b/lib/gitlab/database/reflection.rb
@@ -114,7 +114,7 @@ module Gitlab
'PostgreSQL on Amazon RDS' => { statement: 'SHOW rds.extensions', error: /PG::UndefinedObject/ },
# Based on https://cloud.google.com/sql/docs/postgres/flags#postgres-c this should be specific
# to Cloud SQL for PostgreSQL
- 'Cloud SQL for PostgreSQL' => { statement: 'SHOW cloudsql.iam_authentication', error: /PG::UndefinedObject/ },
+ 'Cloud SQL for PostgreSQL' => { statement: 'SHOW cloudsql.iam_authentication', error: /PG::UndefinedObject/ },
# Based on
# - https://docs.microsoft.com/en-us/azure/postgresql/flexible-server/concepts-extensions
# - https://docs.microsoft.com/en-us/azure/postgresql/concepts-extensions
diff --git a/lib/gitlab/database/reindexing.rb b/lib/gitlab/database/reindexing.rb
index b96dffc99ac..aba45fcc57b 100644
--- a/lib/gitlab/database/reindexing.rb
+++ b/lib/gitlab/database/reindexing.rb
@@ -27,7 +27,7 @@ module Gitlab
# Hack: Before we do actual reindexing work, create async indexes
Gitlab::Database::AsyncIndexes.create_pending_indexes! if Feature.enabled?(:database_async_index_creation, type: :ops)
- Gitlab::Database::AsyncIndexes.drop_pending_indexes! if Feature.enabled?(:database_async_index_destruction, type: :ops)
+ Gitlab::Database::AsyncIndexes.drop_pending_indexes!
automatic_reindexing
end
diff --git a/lib/gitlab/database/tables_sorted_by_foreign_keys.rb b/lib/gitlab/database/tables_sorted_by_foreign_keys.rb
new file mode 100644
index 00000000000..9f096904d31
--- /dev/null
+++ b/lib/gitlab/database/tables_sorted_by_foreign_keys.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Database
+ class TablesSortedByForeignKeys
+ include TSort
+
+ def initialize(connection, tables)
+ @connection = connection
+ @tables = tables
+ end
+
+ def execute
+ strongly_connected_components
+ end
+
+ private
+
+ def tsort_each_node(&block)
+ tables_dependencies.each_key(&block)
+ end
+
+ def tsort_each_child(node, &block)
+ tables_dependencies[node].each(&block)
+ end
+
+ # it maps the tables to the tables that depend on it
+ def tables_dependencies
+ @tables.to_h do |table_name|
+ [table_name, all_foreign_keys[table_name]&.map(&:from_table).to_a]
+ end
+ end
+
+ def all_foreign_keys
+ @all_foreign_keys ||= @tables.flat_map do |table_name|
+ @connection.foreign_keys(table_name)
+ end.group_by(&:to_table)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/tables_truncate.rb b/lib/gitlab/database/tables_truncate.rb
new file mode 100644
index 00000000000..164520fbab3
--- /dev/null
+++ b/lib/gitlab/database/tables_truncate.rb
@@ -0,0 +1,96 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Database
+ class TablesTruncate
+ GITLAB_SCHEMAS_TO_IGNORE = %i[gitlab_geo].freeze
+
+ def initialize(database_name:, min_batch_size:, logger: nil, until_table: nil, dry_run: false)
+ @database_name = database_name
+ @min_batch_size = min_batch_size
+ @logger = logger
+ @until_table = until_table
+ @dry_run = dry_run
+ end
+
+ def execute
+ raise "Cannot truncate legacy tables in single-db setup" unless Gitlab::Database.has_config?(:ci)
+ raise "database is not supported" unless %w[main ci].include?(database_name)
+
+ logger&.info "DRY RUN:" if dry_run
+
+ connection = Gitlab::Database.database_base_models[database_name].connection
+
+ schemas_for_connection = Gitlab::Database.gitlab_schemas_for_connection(connection)
+ tables_to_truncate = Gitlab::Database::GitlabSchema.tables_to_schema.reject do |_, schema_name|
+ (GITLAB_SCHEMAS_TO_IGNORE.union(schemas_for_connection)).include?(schema_name)
+ end.keys
+
+ tables_sorted = Gitlab::Database::TablesSortedByForeignKeys.new(connection, tables_to_truncate).execute
+ # Checking if all the tables have the write-lock triggers
+ # to make sure we are deleting the right tables on the right database.
+ tables_sorted.flatten.each do |table_name|
+ query = <<~SQL
+ SELECT COUNT(*) from information_schema.triggers
+ WHERE event_object_table = '#{table_name}'
+ AND trigger_name = 'gitlab_schema_write_trigger_for_#{table_name}'
+ SQL
+
+ if connection.select_value(query) == 0
+ raise "Table '#{table_name}' is not locked for writes. Run the rake task gitlab:db:lock_writes first"
+ end
+ end
+
+ if until_table
+ table_index = tables_sorted.find_index { |tables_group| tables_group.include?(until_table) }
+ raise "The table '#{until_table}' is not within the truncated tables" if table_index.nil?
+
+ tables_sorted = tables_sorted[0..table_index]
+ end
+
+ # min_batch_size is the minimum number of new tables to truncate at each stage.
+ # But in each stage we have also have to truncate the already truncated tables in the previous stages
+ logger&.info "Truncating legacy tables for the database #{database_name}"
+ truncate_tables_in_batches(connection, tables_sorted, min_batch_size)
+ end
+
+ private
+
+ attr_accessor :database_name, :min_batch_size, :logger, :dry_run, :until_table
+
+ def truncate_tables_in_batches(connection, tables_sorted, min_batch_size)
+ truncated_tables = []
+
+ tables_sorted.flatten.each do |table|
+ sql_statement = "SELECT set_config('lock_writes.#{table}', 'false', false)"
+ logger&.info(sql_statement)
+ connection.execute(sql_statement) unless dry_run
+ end
+
+ # We do the truncation in stages to avoid high IO
+ # In each stage, we truncate the new tables along with the already truncated
+ # tables before. That's because PostgreSQL doesn't allow to truncate any table (A)
+ # without truncating any other table (B) that has a Foreign Key pointing to the table (A).
+ # even if table (B) is empty, because it has been already truncated in a previous stage.
+ tables_sorted.in_groups_of(min_batch_size, false).each do |tables_groups|
+ new_tables_to_truncate = tables_groups.flatten
+ logger&.info "= New tables to truncate: #{new_tables_to_truncate.join(', ')}"
+ truncated_tables.push(*new_tables_to_truncate).tap(&:sort!)
+ sql_statements = [
+ "SET LOCAL statement_timeout = 0",
+ "SET LOCAL lock_timeout = 0",
+ "TRUNCATE TABLE #{truncated_tables.join(', ')} RESTRICT"
+ ]
+
+ sql_statements.each { |sql_statement| logger&.info(sql_statement) }
+
+ next if dry_run
+
+ connection.transaction do
+ sql_statements.each { |sql_statement| connection.execute(sql_statement) }
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database_importers/security/training_providers/importer.rb b/lib/gitlab/database_importers/security/training_providers/importer.rb
new file mode 100644
index 00000000000..aa6a9f29c6d
--- /dev/null
+++ b/lib/gitlab/database_importers/security/training_providers/importer.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module DatabaseImporters
+ module Security
+ module TrainingProviders
+ module Importer
+ KONTRA_DATA = {
+ name: 'Kontra',
+ description: "Kontra Application Security provides interactive developer security education that
+ enables engineers to quickly learn security best practices
+ and fix issues in their code by analysing real-world software security vulnerabilities.",
+ url: "https://application.security/api/webhook/gitlab/exercises/search"
+ }.freeze
+
+ SCW_DATA = {
+ name: 'Secure Code Warrior',
+ description: "Resolve vulnerabilities faster and confidently with
+ highly relevant and bite-sized secure coding learning.",
+ url: "https://integration-api.securecodewarrior.com/api/v1/trial"
+ }.freeze
+
+ module Security
+ class TrainingProvider < ApplicationRecord
+ self.table_name = 'security_training_providers'
+ end
+ end
+
+ def self.upsert_providers
+ current_time = Time.current
+ timestamps = { created_at: current_time, updated_at: current_time }
+
+ Security::TrainingProvider.upsert_all(
+ [KONTRA_DATA.merge(timestamps), SCW_DATA.merge(timestamps)],
+ unique_by: :index_security_training_providers_on_unique_name
+ )
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/diff/file_collection/compare.rb b/lib/gitlab/diff/file_collection/compare.rb
index badebabb192..6d8395d048d 100644
--- a/lib/gitlab/diff/file_collection/compare.rb
+++ b/lib/gitlab/diff/file_collection/compare.rb
@@ -6,9 +6,9 @@ module Gitlab
class Compare < Base
def initialize(compare, project:, diff_options:, diff_refs: nil)
super(compare,
- project: project,
+ project: project,
diff_options: diff_options,
- diff_refs: diff_refs)
+ diff_refs: diff_refs)
end
def unfold_diff_lines(positions)
diff --git a/lib/gitlab/diff/highlight_cache.rb b/lib/gitlab/diff/highlight_cache.rb
index 7cfe0086f57..084ce63e36a 100644
--- a/lib/gitlab/diff/highlight_cache.rb
+++ b/lib/gitlab/diff/highlight_cache.rb
@@ -6,7 +6,7 @@ module Gitlab
include Gitlab::Utils::Gzip
include Gitlab::Utils::StrongMemoize
- EXPIRATION = 1.day
+ EXPIRATION = 1.hour
VERSION = 2
delegate :diffable, to: :@diff_collection
@@ -82,6 +82,16 @@ module Gitlab
private
+ def expiration
+ return 1.day unless Feature.enabled?(:highlight_diffs_renewable_expiration, diffable.project)
+
+ if Feature.enabled?(:highlight_diffs_short_renewable_expiration, diffable.project)
+ EXPIRATION
+ else
+ 8.hours
+ end
+ end
+
def set_highlighted_diff_lines(diff_file, content)
diff_file.highlighted_diff_lines = content.map do |line|
Gitlab::Diff::Line.safe_init_from_hash(line)
@@ -125,9 +135,9 @@ module Gitlab
#
def write_to_redis_hash(hash)
Gitlab::Redis::Cache.with do |redis|
- redis.pipelined do
+ redis.pipelined do |pipeline|
hash.each do |diff_file_id, highlighted_diff_lines_hash|
- redis.hset(
+ pipeline.hset(
key,
diff_file_id,
gzip_compress(highlighted_diff_lines_hash.to_json)
@@ -137,8 +147,7 @@ module Gitlab
end
# HSETs have to have their expiration date manually updated
- #
- redis.expire(key, EXPIRATION)
+ pipeline.expire(key, expiration)
end
record_memory_usage(fetch_memory_usage(redis, key))
@@ -188,11 +197,19 @@ module Gitlab
return {} unless file_paths.any?
results = []
+ cache_key = key
+ highlight_diffs_renewable_expiration_enabled = Feature.enabled?(:highlight_diffs_renewable_expiration, diffable.project)
+ expiration_period = expiration
Gitlab::Redis::Cache.with do |redis|
- results = redis.hmget(key, file_paths)
+ redis.pipelined do |pipeline|
+ results = pipeline.hmget(cache_key, file_paths)
+ pipeline.expire(key, expiration_period) if highlight_diffs_renewable_expiration_enabled
+ end
end
+ results = results.value
+
record_hit_ratio(results)
results.map! do |result|
diff --git a/lib/gitlab/doorkeeper_secret_storing/pbkdf2_sha512.rb b/lib/gitlab/doorkeeper_secret_storing/pbkdf2_sha512.rb
deleted file mode 100644
index 4bfb5f9e64c..00000000000
--- a/lib/gitlab/doorkeeper_secret_storing/pbkdf2_sha512.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module DoorkeeperSecretStoring
- class Pbkdf2Sha512 < ::Doorkeeper::SecretStoring::Base
- STRETCHES = 20_000
- # An empty salt is used because we need to look tokens up solely by
- # their hashed value. Additionally, tokens are always cryptographically
- # pseudo-random and unique, therefore salting provides no
- # additional security.
- SALT = ''
-
- def self.transform_secret(plain_secret)
- return plain_secret unless Feature.enabled?(:hash_oauth_tokens)
-
- Devise::Pbkdf2Encryptable::Encryptors::Pbkdf2Sha512.digest(plain_secret, STRETCHES, SALT)
- end
-
- ##
- # Determines whether this strategy supports restoring
- # secrets from the database. This allows detecting users
- # trying to use a non-restorable strategy with +reuse_access_tokens+.
- def self.allows_restoring_secrets?
- false
- end
- end
- end
-end
diff --git a/lib/gitlab/doorkeeper_secret_storing/secret/pbkdf2_sha512.rb b/lib/gitlab/doorkeeper_secret_storing/secret/pbkdf2_sha512.rb
new file mode 100644
index 00000000000..e0884557496
--- /dev/null
+++ b/lib/gitlab/doorkeeper_secret_storing/secret/pbkdf2_sha512.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+module Gitlab
+ module DoorkeeperSecretStoring
+ module Secret
+ class Pbkdf2Sha512 < ::Doorkeeper::SecretStoring::Base
+ STRETCHES = 20_000
+ # An empty salt is used because we need to look tokens up solely by
+ # their hashed value. Additionally, tokens are always cryptographically
+ # pseudo-random and unique, therefore salting provides no
+ # additional security.
+ SALT = ''
+
+ def self.transform_secret(plain_secret, stored_as_hash = false)
+ return plain_secret if Feature.disabled?(:hash_oauth_secrets) && !stored_as_hash
+
+ Devise::Pbkdf2Encryptable::Encryptors::Pbkdf2Sha512.digest(plain_secret, STRETCHES, SALT)
+ end
+
+ ##
+ # Determines whether this strategy supports restoring
+ # secrets from the database. This allows detecting users
+ # trying to use a non-restorable strategy with +reuse_access_tokens+.
+ def self.allows_restoring_secrets?
+ false
+ end
+
+ ##
+ # Securely compare the given +input+ value with a +stored+ value
+ # processed by +transform_secret+.
+ def self.secret_matches?(input, stored)
+ stored_as_hash = stored.starts_with?('$pbkdf2-')
+ transformed_input = transform_secret(input, stored_as_hash)
+ ActiveSupport::SecurityUtils.secure_compare transformed_input, stored
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/doorkeeper_secret_storing/token/pbkdf2_sha512.rb b/lib/gitlab/doorkeeper_secret_storing/token/pbkdf2_sha512.rb
new file mode 100644
index 00000000000..f9e6d4076f3
--- /dev/null
+++ b/lib/gitlab/doorkeeper_secret_storing/token/pbkdf2_sha512.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module DoorkeeperSecretStoring
+ module Token
+ class Pbkdf2Sha512 < ::Doorkeeper::SecretStoring::Base
+ STRETCHES = 20_000
+ # An empty salt is used because we need to look tokens up solely by
+ # their hashed value. Additionally, tokens are always cryptographically
+ # pseudo-random and unique, therefore salting provides no
+ # additional security.
+ SALT = ''
+
+ def self.transform_secret(plain_secret)
+ return plain_secret unless Feature.enabled?(:hash_oauth_tokens)
+
+ Devise::Pbkdf2Encryptable::Encryptors::Pbkdf2Sha512.digest(plain_secret, STRETCHES, SALT)
+ end
+
+ ##
+ # Determines whether this strategy supports restoring
+ # secrets from the database. This allows detecting users
+ # trying to use a non-restorable strategy with +reuse_access_tokens+.
+ def self.allows_restoring_secrets?
+ false
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/email/attachment_uploader.rb b/lib/gitlab/email/attachment_uploader.rb
index b67ca8d8a7d..931276588f0 100644
--- a/lib/gitlab/email/attachment_uploader.rb
+++ b/lib/gitlab/email/attachment_uploader.rb
@@ -20,8 +20,8 @@ module Gitlab
sanitize_exif_if_needed(content, tmp.path)
file = {
- tempfile: tmp,
- filename: attachment.filename,
+ tempfile: tmp,
+ filename: attachment.filename,
content_type: attachment.content_type
}
diff --git a/lib/gitlab/email/message/in_product_marketing/team.rb b/lib/gitlab/email/message/in_product_marketing/team.rb
index 6a0471ef9c5..ca99dd12c8e 100644
--- a/lib/gitlab/email/message/in_product_marketing/team.rb
+++ b/lib/gitlab/email/message/in_product_marketing/team.rb
@@ -42,18 +42,18 @@ module Gitlab
[
s_('InProductMarketing|Did you know teams that use GitLab are far more efficient?'),
list([
- s_('InProductMarketing|Goldman Sachs went from 1 build every two weeks to thousands of builds a day'),
- s_('InProductMarketing|Ticketmaster decreased their CI build time by 15X')
- ])
+ s_('InProductMarketing|Goldman Sachs went from 1 build every two weeks to thousands of builds a day'),
+ s_('InProductMarketing|Ticketmaster decreased their CI build time by 15X')
+ ])
].join("\n"),
s_("InProductMarketing|We know a thing or two about efficiency and we don't want to keep that to ourselves. Sign up for a free trial of GitLab Ultimate and your teams will be on it from day one."),
[
s_('InProductMarketing|Stop wondering and use GitLab to answer questions like:'),
list([
- s_('InProductMarketing|How long does it take us to close issues/MRs by types like feature requests, bugs, tech debt, security?'),
- s_('InProductMarketing|How many days does it take our team to complete various tasks?'),
- s_('InProductMarketing|What does our value stream timeline look like from product to development to review and production?')
- ])
+ s_('InProductMarketing|How long does it take us to close issues/MRs by types like feature requests, bugs, tech debt, security?'),
+ s_('InProductMarketing|How many days does it take our team to complete various tasks?'),
+ s_('InProductMarketing|What does our value stream timeline look like from product to development to review and production?')
+ ])
].join("\n")
][series]
end
diff --git a/lib/gitlab/email/message/repository_push.rb b/lib/gitlab/email/message/repository_push.rb
index d55cf3202a6..293aa3b53bf 100644
--- a/lib/gitlab/email/message/repository_push.rb
+++ b/lib/gitlab/email/message/repository_push.rb
@@ -79,11 +79,11 @@ module Gitlab
@action_name ||=
case @action
when :create
- 'pushed new'
+ s_('Notify|pushed new')
when :delete
- 'deleted'
+ s_('Notify|deleted')
else
- 'pushed to'
+ s_('Notify|pushed to')
end
end
diff --git a/lib/gitlab/emoji.rb b/lib/gitlab/emoji.rb
index f539d627dcb..2b36b1c99bd 100644
--- a/lib/gitlab/emoji.rb
+++ b/lib/gitlab/emoji.rb
@@ -17,13 +17,13 @@ module Gitlab
def emoji_image_tag(name, src)
image_options = {
- class: 'emoji',
- src: src,
- title: ":#{name}:",
- alt: ":#{name}:",
+ class: 'emoji',
+ src: src,
+ title: ":#{name}:",
+ alt: ":#{name}:",
height: 20,
- width: 20,
- align: 'absmiddle'
+ width: 20,
+ align: 'absmiddle'
}
ActionController::Base.helpers.tag(:img, image_options)
diff --git a/lib/gitlab/encoding_helper.rb b/lib/gitlab/encoding_helper.rb
index f26ab6e3ed1..34c674c3003 100644
--- a/lib/gitlab/encoding_helper.rb
+++ b/lib/gitlab/encoding_helper.rb
@@ -71,6 +71,21 @@ module Gitlab
encode_utf8(data, replace: UNICODE_REPLACEMENT_CHARACTER)
end
+ # This method escapes unsupported UTF-8 characters instead of deleting them
+ def encode_utf8_with_escaping!(message)
+ return encode!(message) if Feature.disabled?(:escape_gitaly_refs)
+
+ message = force_encode_utf8(message)
+ return message if message.valid_encoding?
+
+ unless message.valid_encoding?
+ message = message.chars.map { |char| char.valid_encoding? ? char : escape_chars(char) }.join
+ end
+
+ # encode and clean the bad chars
+ message.replace clean(message)
+ end
+
def encode_utf8(message, replace: "")
message = force_encode_utf8(message)
return message if message.valid_encoding?
@@ -145,6 +160,15 @@ module Gitlab
message.force_encoding("UTF-8")
end
+ # Escapes \x80 - \xFF characters not supported by UTF-8
+ def escape_chars(char)
+ bytes = char.bytes
+
+ return char unless bytes.one?
+
+ "%#{bytes.first.to_s(16).upcase}"
+ end
+
def clean(message, replace: "")
message.encode(
"UTF-16BE",
diff --git a/lib/gitlab/etag_caching/middleware.rb b/lib/gitlab/etag_caching/middleware.rb
index a1918ee6ad5..f6431483a15 100644
--- a/lib/gitlab/etag_caching/middleware.rb
+++ b/lib/gitlab/etag_caching/middleware.rb
@@ -97,12 +97,12 @@ module Gitlab
def add_instrument_for_cache_hit(status, route, request)
payload = {
etag_route: route.name,
- params: request.filtered_parameters,
- headers: request.headers,
- format: request.format.ref,
- method: request.request_method,
- path: request.filtered_path,
- status: status
+ params: request.filtered_parameters,
+ headers: request.headers,
+ format: request.format.ref,
+ method: request.request_method,
+ path: request.filtered_path,
+ status: status
}
ActiveSupport::Notifications.instrument(
diff --git a/lib/gitlab/etag_caching/store.rb b/lib/gitlab/etag_caching/store.rb
index 44c6984c09b..437d577e70e 100644
--- a/lib/gitlab/etag_caching/store.rb
+++ b/lib/gitlab/etag_caching/store.rb
@@ -16,9 +16,9 @@ module Gitlab
etags = keys.map { generate_etag }
Gitlab::Redis::SharedState.with do |redis|
- redis.pipelined do
+ redis.pipelined do |pipeline|
keys.each_with_index do |key, i|
- redis.set(redis_shared_state_key(key), etags[i], ex: EXPIRY_TIME, nx: only_if_missing)
+ pipeline.set(redis_shared_state_key(key), etags[i], ex: EXPIRY_TIME, nx: only_if_missing)
end
end
end
diff --git a/lib/gitlab/experimentation.rb b/lib/gitlab/experimentation.rb
index 8a5432025d8..142d0e55593 100644
--- a/lib/gitlab/experimentation.rb
+++ b/lib/gitlab/experimentation.rb
@@ -70,9 +70,9 @@ module Gitlab
logger = Gitlab::ExperimentationLogger.build
logger.warn message: 'Subject must conform to the rollout strategy',
- experiment_key: experiment_key,
- subject: subject.class.to_s,
- rollout_strategy: rollout_strategy(experiment_key)
+ experiment_key: experiment_key,
+ subject: subject.class.to_s,
+ rollout_strategy: rollout_strategy(experiment_key)
end
def valid_subject_for_rollout_strategy?(experiment_key, subject)
diff --git a/lib/gitlab/external_authorization/cache.rb b/lib/gitlab/external_authorization/cache.rb
index 509daeb0248..c06711d16f8 100644
--- a/lib/gitlab/external_authorization/cache.rb
+++ b/lib/gitlab/external_authorization/cache.rb
@@ -20,8 +20,8 @@ module Gitlab
def store(new_access, new_reason, new_refreshed_at)
::Gitlab::Redis::Cache.with do |redis|
- redis.pipelined do
- redis.mapped_hmset(
+ redis.pipelined do |pipeline|
+ pipeline.mapped_hmset(
cache_key,
{
access: new_access.to_s,
@@ -30,7 +30,7 @@ module Gitlab
}
)
- redis.expire(cache_key, VALIDITY_TIME)
+ pipeline.expire(cache_key, VALIDITY_TIME)
end
end
end
diff --git a/lib/gitlab/fogbugz_import/importer.rb b/lib/gitlab/fogbugz_import/importer.rb
index 612865ed1be..ca1a2b2a077 100644
--- a/lib/gitlab/fogbugz_import/importer.rb
+++ b/lib/gitlab/fogbugz_import/importer.rb
@@ -129,15 +129,15 @@ module Gitlab
author_id = user_info(bug['ixPersonOpenedBy'])[:gitlab_id] || project.creator_id
issue = Issue.create!(
- iid: bug['ixBug'],
- project_id: project.id,
- title: bug['sTitle'],
- description: body,
- author_id: author_id,
+ iid: bug['ixBug'],
+ project_id: project.id,
+ title: bug['sTitle'],
+ description: body,
+ author_id: author_id,
assignee_ids: [assignee_id],
- state: bug['fOpen'] == 'true' ? 'opened' : 'closed',
- created_at: date,
- updated_at: DateTime.parse(bug['dtLastUpdated'])
+ state: bug['fOpen'] == 'true' ? 'opened' : 'closed',
+ created_at: date,
+ updated_at: DateTime.parse(bug['dtLastUpdated'])
)
issue_labels = ::LabelsFinder.new(nil, project_id: project.id, title: labels).execute(skip_authorization: true)
@@ -184,11 +184,11 @@ module Gitlab
)
note = Note.create!(
- project_id: project.id,
- noteable_type: "Issue",
- noteable_id: issue.id,
- author_id: author_id,
- note: body
+ project_id: project.id,
+ noteable_type: "Issue",
+ noteable_id: issue.id,
+ author_id: author_id,
+ note: body
)
note.update_attribute(:created_at, date)
diff --git a/lib/gitlab/gfm/reference_rewriter.rb b/lib/gitlab/gfm/reference_rewriter.rb
index 40dcac5f46f..0c13ab604bc 100644
--- a/lib/gitlab/gfm/reference_rewriter.rb
+++ b/lib/gitlab/gfm/reference_rewriter.rb
@@ -55,7 +55,12 @@ module Gitlab
end
def needs_rewrite?
- strong_memoize(:needs_rewrite) { @text_html.include?('data-reference-type=') }
+ strong_memoize(:needs_rewrite) do
+ reference_type_attribute =
+ Banzai::Filter::References::ReferenceFilter::REFERENCE_TYPE_DATA_ATTRIBUTE
+
+ @text_html.include?(reference_type_attribute)
+ end
end
private
diff --git a/lib/gitlab/git.rb b/lib/gitlab/git.rb
index 4b9f2ababc8..4b877bf44da 100644
--- a/lib/gitlab/git.rb
+++ b/lib/gitlab/git.rb
@@ -25,7 +25,7 @@ module Gitlab
include Gitlab::EncodingHelper
def ref_name(ref)
- encode!(ref).sub(%r{\Arefs/(tags|heads|remotes)/}, '')
+ encode_utf8_with_escaping!(ref).sub(%r{\Arefs/(tags|heads|remotes)/}, '')
end
def branch_name(ref)
diff --git a/lib/gitlab/git/diff.rb b/lib/gitlab/git/diff.rb
index 003cc87d65a..72f7413500f 100644
--- a/lib/gitlab/git/diff.rb
+++ b/lib/gitlab/git/diff.rb
@@ -230,7 +230,6 @@ module Gitlab
private
def encode_diff_to_utf8(replace_invalid_utf8_chars)
- return unless Feature.enabled?(:convert_diff_to_utf8_with_replacement_symbol)
return unless replace_invalid_utf8_chars && diff_should_be_converted?
@diff = Gitlab::EncodingHelper.encode_utf8_with_replacement_character(@diff)
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index ad655fedb6d..f1cd75258be 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -403,7 +403,7 @@ module Gitlab
wrapped_gitaly_errors do
gitaly_blob_client.list_blobs(revisions, limit: REV_LIST_COMMIT_LIMIT,
- with_paths: with_paths, dynamic_timeout: dynamic_timeout)
+ with_paths: with_paths, dynamic_timeout: dynamic_timeout)
end
end
@@ -701,7 +701,9 @@ module Gitlab
# Delete the specified branch from the repository
# Note: No Git hooks are executed for this action
def delete_branch(branch_name)
- write_ref(branch_name, Gitlab::Git::BLANK_SHA)
+ branch_name = "#{Gitlab::Git::BRANCH_REF_PREFIX}#{branch_name}" unless branch_name.start_with?("refs/")
+
+ delete_refs(branch_name)
rescue CommandError => e
raise DeleteBranchError, e
end
@@ -913,8 +915,29 @@ module Gitlab
true
end
+ # Creates a commit
+ #
+ # @param [User] user The committer of the commit.
+ # @param [String] branch_name: The name of the branch to be created/updated.
+ # @param [String] message: The commit message.
+ # @param [Array<Hash>] actions: An array of files to be added/updated/removed.
+ # @option actions: [Symbol] :action One of :create, :create_dir, :update, :move, :delete, :chmod
+ # @option actions: [String] :file_path The path of the file or directory being added/updated/removed.
+ # @option actions: [String] :previous_path The path of the file being moved. Only used for the :move action.
+ # @option actions: [String,IO] :content The file content for :create or :update
+ # @option actions: [String] :encoding One of text, base64
+ # @option actions: [Boolean] :execute_filemode True sets the executable filemode on the file.
+ # @option actions: [Boolean] :infer_content True uses the existing file contents instead of using content on move.
+ # @param [String] author_email: The authors email, if unspecified the committers email is used.
+ # @param [String] author_name: The authors name, if unspecified the committers name is used.
+ # @param [String] start_branch_name: The name of the branch to be used as the parent of the commit. Only used if start_sha: is unspecified.
+ # @param [String] start_sha: The sha to be used as the parent of the commit.
+ # @param [Gitlab::Git::Repository] start_repository: The repository that contains the start branch or sha. Defaults to use this repository.
+ # @param [Boolean] force: Force update the branch.
+ # @return [Gitlab::Git::OperationService::BranchUpdate]
+ #
# rubocop:disable Metrics/ParameterLists
- def multi_action(
+ def commit_files(
user, branch_name:, message:, actions:,
author_email: nil, author_name: nil,
start_branch_name: nil, start_sha: nil, start_repository: nil,
@@ -989,8 +1012,8 @@ module Gitlab
gitaly_ref_client.branch_names_contains_sha(sha)
end
- def tag_names_contains_sha(sha)
- gitaly_ref_client.tag_names_contains_sha(sha)
+ def tag_names_contains_sha(sha, limit: 0)
+ gitaly_ref_client.tag_names_contains_sha(sha, limit: limit)
end
def search_files_by_content(query, ref, options = {})
@@ -1011,16 +1034,20 @@ module Gitlab
end
def search_files_by_name(query, ref)
- safe_query = Regexp.escape(query.sub(%r{^/*}, ""))
+ safe_query = query.sub(%r{^/*}, "")
ref ||= root_ref
return [] if empty? || safe_query.blank?
- gitaly_repository_client.search_files_by_name(ref, safe_query)
+ gitaly_repository_client.search_files_by_name(ref, safe_query).map do |file|
+ Gitlab::EncodingHelper.encode_utf8(file)
+ end
end
def search_files_by_regexp(filter, ref = 'HEAD')
- gitaly_repository_client.search_files_by_regexp(ref, filter)
+ gitaly_repository_client.search_files_by_regexp(ref, filter).map do |file|
+ Gitlab::EncodingHelper.encode_utf8(file)
+ end
end
def find_commits_by_message(query, ref, path, limit, offset)
@@ -1031,6 +1058,24 @@ module Gitlab
end
end
+ def list_commits_by(query, ref, author: nil, before: nil, after: nil, limit: 1000)
+ params = {
+ author: author,
+ ignore_case: true,
+ commit_message_patterns: query,
+ before: before,
+ after: after,
+ reverse: false,
+ pagination_params: { limit: limit }
+ }
+
+ wrapped_gitaly_errors do
+ gitaly_commit_client
+ .list_commits([ref], params)
+ .map { |c| commit(c) }
+ end
+ end
+
def list_last_commits_for_tree(sha, path, offset: 0, limit: 25, literal_pathspec: false)
wrapped_gitaly_errors do
gitaly_commit_client.list_last_commits_for_tree(sha, path, offset: offset, limit: limit, literal_pathspec: literal_pathspec)
diff --git a/lib/gitlab/git/tag.rb b/lib/gitlab/git/tag.rb
index 25895dc6728..5ed5158eeea 100644
--- a/lib/gitlab/git/tag.rb
+++ b/lib/gitlab/git/tag.rb
@@ -63,7 +63,7 @@ module Gitlab
end
def init_from_gitaly
- @name = encode!(@raw_tag.name.dup)
+ @name = encode_utf8_with_escaping!(@raw_tag.name.dup)
@target = @raw_tag.id
@message = message_from_gitaly_tag
diff --git a/lib/gitlab/git/wiki.rb b/lib/gitlab/git/wiki.rb
index 4bab94968d7..2228fcb886e 100644
--- a/lib/gitlab/git/wiki.rb
+++ b/lib/gitlab/git/wiki.rb
@@ -70,18 +70,6 @@ module Gitlab
@repository.exists?
end
- def write_page(name, format, content, commit_details)
- wrapped_gitaly_errors do
- gitaly_write_page(name, format, content, commit_details)
- end
- end
-
- def update_page(page_path, title, format, content, commit_details)
- wrapped_gitaly_errors do
- gitaly_update_page(page_path, title, format, content, commit_details)
- end
- end
-
def list_pages(limit: 0, sort: nil, direction_desc: false, load_content: false)
wrapped_gitaly_errors do
gitaly_list_pages(
@@ -113,21 +101,13 @@ module Gitlab
@gitaly_wiki_client ||= Gitlab::GitalyClient::WikiService.new(@repository)
end
- def gitaly_write_page(name, format, content, commit_details)
- gitaly_wiki_client.write_page(name, format, content, commit_details)
- end
-
- def gitaly_update_page(page_path, title, format, content, commit_details)
- gitaly_wiki_client.update_page(page_path, title, format, content, commit_details)
- end
-
def gitaly_find_page(title:, version: nil, dir: nil, load_content: true)
return unless title.present?
wiki_page, version = gitaly_wiki_client.find_page(title: title, version: version, dir: dir, load_content: load_content)
return unless wiki_page
- Gitlab::Git::WikiPage.new(wiki_page, version)
+ Gitlab::Git::WikiPage.from_gitaly_wiki_page(wiki_page, version)
rescue GRPC::InvalidArgument
nil
end
@@ -143,7 +123,7 @@ module Gitlab
end
gitaly_pages.map do |wiki_page, version|
- Gitlab::Git::WikiPage.new(wiki_page, version)
+ Gitlab::Git::WikiPage.from_gitaly_wiki_page(wiki_page, version)
end
end
end
diff --git a/lib/gitlab/git/wiki_page.rb b/lib/gitlab/git/wiki_page.rb
index a1f3d64ccde..57b7e7d53dd 100644
--- a/lib/gitlab/git/wiki_page.rb
+++ b/lib/gitlab/git/wiki_page.rb
@@ -5,17 +5,31 @@ module Gitlab
class WikiPage
attr_reader :url_path, :title, :format, :path, :version, :raw_data, :name, :historical, :formatted_data
- # This class abstracts away Gitlab::GitalyClient::WikiPage
- def initialize(gitaly_page, version)
- @url_path = gitaly_page.url_path
- @title = gitaly_page.title
- @format = gitaly_page.format
- @path = gitaly_page.path
- @raw_data = gitaly_page.raw_data
- @name = gitaly_page.name
- @historical = gitaly_page.historical?
+ class << self
+ # Abstracts away Gitlab::GitalyClient::WikiPage
+ def from_gitaly_wiki_page(gitaly_page, version)
+ new(
+ url_path: gitaly_page.url_path,
+ title: gitaly_page.title,
+ format: gitaly_page.format,
+ path: gitaly_page.path,
+ raw_data: gitaly_page.raw_data,
+ name: gitaly_page.name,
+ historical: gitaly_page.historical?,
+ version: version
+ )
+ end
+ end
- @version = version
+ def initialize(hash)
+ @url_path = hash[:url_path]
+ @title = hash[:title]
+ @format = hash[:format]
+ @path = hash[:path]
+ @raw_data = hash[:raw_data]
+ @name = hash[:name]
+ @historical = hash[:historical]
+ @version = hash[:version]
end
def historical?
diff --git a/lib/gitlab/gitaly_client/commit_service.rb b/lib/gitlab/gitaly_client/commit_service.rb
index 0f306a9825d..312d1dddff1 100644
--- a/lib/gitlab/gitaly_client/commit_service.rb
+++ b/lib/gitlab/gitaly_client/commit_service.rb
@@ -232,7 +232,7 @@ module Gitlab
msg.paths.map do |path|
Gitlab::Git::ChangedPath.new(
status: path.status,
- path: EncodingHelper.encode!(path.path)
+ path: EncodingHelper.encode!(path.path)
)
end
end
@@ -251,14 +251,23 @@ module Gitlab
consume_commits_response(response)
end
- def list_commits(revisions, reverse: false, pagination_params: nil)
+ def list_commits(revisions, params = {})
request = Gitaly::ListCommitsRequest.new(
repository: @gitaly_repo,
revisions: Array.wrap(revisions),
- reverse: reverse,
- pagination_params: pagination_params
+ reverse: !!params[:reverse],
+ ignore_case: params[:ignore_case],
+ pagination_params: params[:pagination_params]
)
+ if params[:commit_message_patterns]
+ request.commit_message_patterns += Array.wrap(params[:commit_message_patterns])
+ end
+
+ request.author = encode_binary(params[:author]) if params[:author]
+ request.before = GitalyClient.timestamp(params[:before]) if params[:before]
+ request.after = GitalyClient.timestamp(params[:after]) if params[:after]
+
response = GitalyClient.call(@repository.storage, :commit_service, :list_commits, request, timeout: GitalyClient.medium_timeout)
consume_commits_response(response)
end
@@ -396,12 +405,12 @@ module Gitlab
def find_commits(options)
request = Gitaly::FindCommitsRequest.new(
- repository: @gitaly_repo,
- limit: options[:limit],
- offset: options[:offset],
- follow: options[:follow],
- skip_merges: options[:skip_merges],
- all: !!options[:all],
+ repository: @gitaly_repo,
+ limit: options[:limit],
+ offset: options[:offset],
+ follow: options[:follow],
+ skip_merges: options[:skip_merges],
+ all: !!options[:all],
first_parent: !!options[:first_parent],
global_options: parse_global_options!(options),
disable_walk: true, # This option is deprecated. The 'walk' implementation is being removed.
diff --git a/lib/gitlab/gitaly_client/operation_service.rb b/lib/gitlab/gitaly_client/operation_service.rb
index c5c6ec1cdfa..7835fb32f59 100644
--- a/lib/gitlab/gitaly_client/operation_service.rb
+++ b/lib/gitlab/gitaly_client/operation_service.rb
@@ -85,8 +85,20 @@ module Gitlab
target_commit = Gitlab::Git::Commit.decorate(@repository, branch.target_commit)
Gitlab::Git::Branch.new(@repository, branch.name, target_commit.id, target_commit)
- rescue GRPC::FailedPrecondition => ex
- raise Gitlab::Git::Repository::InvalidRef, ex
+ rescue GRPC::BadStatus => e
+ detailed_error = GitalyClient.decode_detailed_error(e)
+
+ case detailed_error&.error
+ when :custom_hook
+ raise Gitlab::Git::PreReceiveError.new(custom_hook_error_message(detailed_error.custom_hook),
+ fallback_message: e.details)
+ else
+ if e.code == GRPC::Core::StatusCodes::FAILED_PRECONDITION
+ raise Gitlab::Git::Repository::InvalidRef, e
+ end
+
+ raise
+ end
end
def user_update_branch(branch_name, user, newrev, oldrev)
@@ -410,9 +422,9 @@ module Gitlab
end
end
- response = GitalyClient.call(@repository.storage, :operation_service,
- :user_commit_files, req_enum, timeout: GitalyClient.long_timeout,
- remote_storage: start_repository&.storage)
+ response = GitalyClient.call(
+ @repository.storage, :operation_service, :user_commit_files, req_enum,
+ timeout: GitalyClient.long_timeout, remote_storage: start_repository&.storage)
if (pre_receive_error = response.pre_receive_error.presence)
raise Gitlab::Git::PreReceiveError, pre_receive_error
diff --git a/lib/gitlab/gitaly_client/ref_service.rb b/lib/gitlab/gitaly_client/ref_service.rb
index 42f9c165610..bb6bc3121bd 100644
--- a/lib/gitlab/gitaly_client/ref_service.rb
+++ b/lib/gitlab/gitaly_client/ref_service.rb
@@ -7,7 +7,8 @@ module Gitlab
TAGS_SORT_KEY = {
'name' => Gitaly::FindAllTagsRequest::SortBy::Key::REFNAME,
- 'updated' => Gitaly::FindAllTagsRequest::SortBy::Key::CREATORDATE
+ 'updated' => Gitaly::FindAllTagsRequest::SortBy::Key::CREATORDATE,
+ 'version' => Gitaly::FindAllTagsRequest::SortBy::Key::VERSION_REFNAME
}.freeze
TAGS_SORT_DIRECTION = {
@@ -104,7 +105,7 @@ module Gitlab
return unless branch
target_commit = Gitlab::Git::Commit.decorate(@repository, branch.target_commit)
- Gitlab::Git::Branch.new(@repository, encode!(branch.name.dup), branch.target_commit.id, target_commit)
+ Gitlab::Git::Branch.new(@repository, branch.name.dup, branch.target_commit.id, target_commit)
end
def find_tag(tag_name)
@@ -258,7 +259,7 @@ module Gitlab
end
def sort_tags_by_param(sort_by)
- match = sort_by.match(/^(?<key>name|updated)_(?<direction>asc|desc)$/)
+ match = sort_by.match(/^(?<key>name|updated|version)_(?<direction>asc|desc)$/)
return unless match
@@ -269,14 +270,23 @@ module Gitlab
end
def consume_find_local_branches_response(response)
- response.flat_map do |message|
- message.branches.map do |gitaly_branch|
- Gitlab::Git::Branch.new(
- @repository,
- encode!(gitaly_branch.name.dup),
- gitaly_branch.commit_id,
- commit_from_local_branches_response(gitaly_branch)
- )
+ if Feature.enabled?(:gitaly_simplify_find_local_branches_response, type: :undefined)
+ response.flat_map do |message|
+ message.local_branches.map do |branch|
+ target_commit = Gitlab::Git::Commit.decorate(@repository, branch.target_commit)
+ Gitlab::Git::Branch.new(@repository, branch.name, branch.target_commit.id, target_commit)
+ end
+ end
+ else
+ response.flat_map do |message|
+ message.branches.map do |gitaly_branch|
+ Gitlab::Git::Branch.new(
+ @repository,
+ gitaly_branch.name.dup,
+ gitaly_branch.commit_id,
+ commit_from_local_branches_response(gitaly_branch)
+ )
+ end
end
end
end
diff --git a/lib/gitlab/gitaly_client/server_service.rb b/lib/gitlab/gitaly_client/server_service.rb
index 36bda67c26e..48fd0e66354 100644
--- a/lib/gitlab/gitaly_client/server_service.rb
+++ b/lib/gitlab/gitaly_client/server_service.rb
@@ -26,6 +26,19 @@ module Gitlab
storage_specific(disk_statistics)
end
+ def readiness_check
+ request = Gitaly::ReadinessCheckRequest.new(timeout: GitalyClient.medium_timeout)
+ response = GitalyClient.call(@storage, :server_service, :readiness_check, request, timeout: GitalyClient.default_timeout)
+
+ return { success: true } if response.ok_response
+
+ failed_checks = response.failure_response.failed_checks.map do |failed_check|
+ ["#{failed_check.name}: #{failed_check.error_message}"]
+ end
+
+ { success: false, message: failed_checks.join("\n") }
+ end
+
private
def storage_specific(response)
diff --git a/lib/gitlab/github_import/attachments_downloader.rb b/lib/gitlab/github_import/attachments_downloader.rb
new file mode 100644
index 00000000000..b71d5f753f2
--- /dev/null
+++ b/lib/gitlab/github_import/attachments_downloader.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ class AttachmentsDownloader
+ include ::Gitlab::ImportExport::CommandLineUtil
+ include ::BulkImports::FileDownloads::FilenameFetch
+ include ::BulkImports::FileDownloads::Validations
+
+ DownloadError = Class.new(StandardError)
+
+ FILENAME_SIZE_LIMIT = 255 # chars before the extension
+ DEFAULT_FILE_SIZE_LIMIT = 25.megabytes
+ TMP_DIR = File.join(Dir.tmpdir, 'github_attachments').freeze
+
+ attr_reader :file_url, :filename, :file_size_limit
+
+ def initialize(file_url, file_size_limit: DEFAULT_FILE_SIZE_LIMIT)
+ @file_url = file_url
+ @file_size_limit = file_size_limit
+
+ filename = URI(file_url).path.split('/').last
+ @filename = ensure_filename_size(filename)
+ end
+
+ def perform
+ validate_content_length
+ validate_filepath
+
+ file = download
+ validate_symlink
+ file
+ end
+
+ def delete
+ FileUtils.rm_rf File.dirname(filepath)
+ end
+
+ private
+
+ def raise_error(message)
+ raise DownloadError, message
+ end
+
+ def response_headers
+ @response_headers ||=
+ Gitlab::HTTP.perform_request(Net::HTTP::Head, file_url, {}).headers
+ end
+
+ def download
+ file = File.open(filepath, 'wb')
+ Gitlab::HTTP.perform_request(Net::HTTP::Get, file_url, stream_body: true) { |batch| file.write(batch) }
+ file
+ end
+
+ def filepath
+ @filepath ||= begin
+ dir = File.join(TMP_DIR, SecureRandom.uuid)
+ mkdir_p dir
+ File.join(dir, filename)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/client.rb b/lib/gitlab/github_import/client.rb
index 11a41149274..6cff15a204f 100644
--- a/lib/gitlab/github_import/client.rb
+++ b/lib/gitlab/github_import/client.rb
@@ -76,11 +76,15 @@ module Gitlab
each_object(:pull_request_reviews, repo_name, iid)
end
+ def repos(options = {})
+ octokit.repos(nil, options).map(&:to_h)
+ end
+
# Returns the details of a GitHub repository.
#
# name - The path (in the form `owner/repository`) of the repository.
def repository(name)
- with_rate_limit { octokit.repo(name) }
+ with_rate_limit { octokit.repo(name).to_h }
end
def pull_request(repo_name, iid)
@@ -99,6 +103,14 @@ module Gitlab
each_object(:releases, *args)
end
+ def branches(*args)
+ each_object(:branches, *args)
+ end
+
+ def branch_protection(repo_name, branch_name)
+ with_rate_limit { octokit.branch_protection(repo_name, branch_name) }
+ end
+
# Fetches data from the GitHub API and yields a Page object for every page
# of data, without loading all of them into memory.
#
@@ -167,7 +179,7 @@ module Gitlab
end
def search_repos_by_name(name, options = {})
- with_retry { octokit.search_repositories(search_query(str: name, type: :name), options) }
+ with_retry { octokit.search_repositories(search_query(str: name, type: :name), options).to_h }
end
def search_query(str:, type:, include_collaborations: true, include_orgs: true)
diff --git a/lib/gitlab/github_import/importer/events/base_importer.rb b/lib/gitlab/github_import/importer/events/base_importer.rb
index 9ab1d916d33..8218acf2bfb 100644
--- a/lib/gitlab/github_import/importer/events/base_importer.rb
+++ b/lib/gitlab/github_import/importer/events/base_importer.rb
@@ -29,6 +29,19 @@ module Gitlab
def issuable_db_id(object)
IssuableFinder.new(project, object).database_id
end
+
+ def issuable_type(issue_event)
+ merge_request_event?(issue_event) ? MergeRequest.name : Issue.name
+ end
+
+ def merge_request_event?(issue_event)
+ issue_event.issuable_type == MergeRequest.name
+ end
+
+ def resource_event_belongs_to(issue_event)
+ belongs_to_key = merge_request_event?(issue_event) ? :merge_request_id : :issue_id
+ { belongs_to_key => issuable_db_id(issue_event) }
+ end
end
end
end
diff --git a/lib/gitlab/github_import/importer/events/changed_assignee.rb b/lib/gitlab/github_import/importer/events/changed_assignee.rb
index c8f6335e4a8..b75d41f40de 100644
--- a/lib/gitlab/github_import/importer/events/changed_assignee.rb
+++ b/lib/gitlab/github_import/importer/events/changed_assignee.rb
@@ -7,22 +7,22 @@ module Gitlab
class ChangedAssignee < BaseImporter
def execute(issue_event)
assignee_id = author_id(issue_event, author_key: :assignee)
- assigner_id = author_id(issue_event, author_key: :assigner)
+ author_id = author_id(issue_event, author_key: :actor)
- note_body = parse_body(issue_event, assigner_id, assignee_id)
+ note_body = parse_body(issue_event, assignee_id)
- create_note(issue_event, note_body, assigner_id)
+ create_note(issue_event, note_body, author_id)
end
private
- def create_note(issue_event, note_body, assigner_id)
+ def create_note(issue_event, note_body, author_id)
Note.create!(
system: true,
- noteable_type: Issue.name,
+ noteable_type: issuable_type(issue_event),
noteable_id: issuable_db_id(issue_event),
project: project,
- author_id: assigner_id,
+ author_id: author_id,
note: note_body,
system_note_metadata: SystemNoteMetadata.new(
{
@@ -36,12 +36,14 @@ module Gitlab
)
end
- def parse_body(issue_event, assigner_id, assignee_id)
+ def parse_body(issue_event, assignee_id)
+ assignee = User.find(assignee_id).to_reference
+
Gitlab::I18n.with_default_locale do
if issue_event.event == "unassigned"
- "unassigned #{User.find(assigner_id).to_reference}"
+ "unassigned #{assignee}"
else
- "assigned to #{User.find(assignee_id).to_reference}"
+ "assigned to #{assignee}"
end
end
end
diff --git a/lib/gitlab/github_import/importer/events/changed_label.rb b/lib/gitlab/github_import/importer/events/changed_label.rb
index 818a9202745..83130d18db9 100644
--- a/lib/gitlab/github_import/importer/events/changed_label.rb
+++ b/lib/gitlab/github_import/importer/events/changed_label.rb
@@ -12,13 +12,14 @@ module Gitlab
private
def create_event(issue_event)
- ResourceLabelEvent.create!(
- issue_id: issuable_db_id(issue_event),
+ attrs = {
user_id: author_id(issue_event),
label_id: label_finder.id_for(issue_event.label_title),
action: action(issue_event.event),
created_at: issue_event.created_at
- )
+ }.merge(resource_event_belongs_to(issue_event))
+
+ ResourceLabelEvent.create!(attrs)
end
def label_finder
diff --git a/lib/gitlab/github_import/importer/events/changed_milestone.rb b/lib/gitlab/github_import/importer/events/changed_milestone.rb
index 3164c041dc3..39b92d88b58 100644
--- a/lib/gitlab/github_import/importer/events/changed_milestone.rb
+++ b/lib/gitlab/github_import/importer/events/changed_milestone.rb
@@ -17,14 +17,15 @@ module Gitlab
private
def create_event(issue_event)
- ResourceMilestoneEvent.create!(
- issue_id: issuable_db_id(issue_event),
+ attrs = {
user_id: author_id(issue_event),
created_at: issue_event.created_at,
milestone_id: project.milestones.find_by_title(issue_event.milestone_title)&.id,
action: action(issue_event.event),
state: DEFAULT_STATE
- )
+ }.merge(resource_event_belongs_to(issue_event))
+
+ ResourceMilestoneEvent.create!(attrs)
end
def action(event_type)
diff --git a/lib/gitlab/github_import/importer/events/changed_reviewer.rb b/lib/gitlab/github_import/importer/events/changed_reviewer.rb
new file mode 100644
index 00000000000..17b1fa4ab45
--- /dev/null
+++ b/lib/gitlab/github_import/importer/events/changed_reviewer.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module Importer
+ module Events
+ class ChangedReviewer < BaseImporter
+ def execute(issue_event)
+ requested_reviewer_id = author_id(issue_event, author_key: :requested_reviewer)
+ review_requester_id = author_id(issue_event, author_key: :review_requester)
+
+ note_body = parse_body(issue_event, requested_reviewer_id)
+
+ create_note(issue_event, note_body, review_requester_id)
+ end
+
+ private
+
+ def create_note(issue_event, note_body, review_requester_id)
+ Note.create!(
+ system: true,
+ noteable_type: issuable_type(issue_event),
+ noteable_id: issuable_db_id(issue_event),
+ project: project,
+ author_id: review_requester_id,
+ note: note_body,
+ system_note_metadata: SystemNoteMetadata.new(
+ {
+ action: 'reviewer',
+ created_at: issue_event.created_at,
+ updated_at: issue_event.created_at
+ }
+ ),
+ created_at: issue_event.created_at,
+ updated_at: issue_event.created_at
+ )
+ end
+
+ def parse_body(issue_event, requested_reviewer_id)
+ requested_reviewer = User.find(requested_reviewer_id).to_reference
+
+ if issue_event.event == 'review_request_removed'
+ "#{SystemNotes::IssuablesService.issuable_events[:review_request_removed]}" \
+ " #{requested_reviewer}"
+ else
+ "#{SystemNotes::IssuablesService.issuable_events[:review_requested]}" \
+ " #{requested_reviewer}"
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/importer/events/closed.rb b/lib/gitlab/github_import/importer/events/closed.rb
index ca8730d0f27..58d9dbf826c 100644
--- a/lib/gitlab/github_import/importer/events/closed.rb
+++ b/lib/gitlab/github_import/importer/events/closed.rb
@@ -17,7 +17,7 @@ module Gitlab
project_id: project.id,
author_id: author_id(issue_event),
action: 'closed',
- target_type: Issue.name,
+ target_type: issuable_type(issue_event),
target_id: issuable_db_id(issue_event),
created_at: issue_event.created_at,
updated_at: issue_event.created_at
@@ -25,15 +25,16 @@ module Gitlab
end
def create_state_event(issue_event)
- ResourceStateEvent.create!(
+ attrs = {
user_id: author_id(issue_event),
- issue_id: issuable_db_id(issue_event),
source_commit: issue_event.commit_id,
state: 'closed',
close_after_error_tracking_resolve: false,
close_auto_resolve_prometheus_alert: false,
created_at: issue_event.created_at
- )
+ }.merge(resource_event_belongs_to(issue_event))
+
+ ResourceStateEvent.create!(attrs)
end
end
end
diff --git a/lib/gitlab/github_import/importer/events/cross_referenced.rb b/lib/gitlab/github_import/importer/events/cross_referenced.rb
index 89fc1bdeb09..b56ae186d3c 100644
--- a/lib/gitlab/github_import/importer/events/cross_referenced.rb
+++ b/lib/gitlab/github_import/importer/events/cross_referenced.rb
@@ -33,7 +33,7 @@ module Gitlab
def create_note(issue_event, note_body, user_id)
Note.create!(
system: true,
- noteable_type: Issue.name,
+ noteable_type: issuable_type(issue_event),
noteable_id: issuable_db_id(issue_event),
project: project,
author_id: user_id,
diff --git a/lib/gitlab/github_import/importer/events/renamed.rb b/lib/gitlab/github_import/importer/events/renamed.rb
index 96d112b04c6..fb9e08116ba 100644
--- a/lib/gitlab/github_import/importer/events/renamed.rb
+++ b/lib/gitlab/github_import/importer/events/renamed.rb
@@ -14,7 +14,7 @@ module Gitlab
def note_params(issue_event)
{
noteable_id: issuable_db_id(issue_event),
- noteable_type: Issue.name,
+ noteable_type: issuable_type(issue_event),
project_id: project.id,
author_id: author_id(issue_event),
note: parse_body(issue_event),
diff --git a/lib/gitlab/github_import/importer/events/reopened.rb b/lib/gitlab/github_import/importer/events/reopened.rb
index b75344bf817..8abeba0777d 100644
--- a/lib/gitlab/github_import/importer/events/reopened.rb
+++ b/lib/gitlab/github_import/importer/events/reopened.rb
@@ -17,7 +17,7 @@ module Gitlab
project_id: project.id,
author_id: author_id(issue_event),
action: 'reopened',
- target_type: Issue.name,
+ target_type: issuable_type(issue_event),
target_id: issuable_db_id(issue_event),
created_at: issue_event.created_at,
updated_at: issue_event.created_at
@@ -25,12 +25,13 @@ module Gitlab
end
def create_state_event(issue_event)
- ResourceStateEvent.create!(
+ attrs = {
user_id: author_id(issue_event),
- issue_id: issuable_db_id(issue_event),
state: 'reopened',
created_at: issue_event.created_at
- )
+ }.merge(resource_event_belongs_to(issue_event))
+
+ ResourceStateEvent.create!(attrs)
end
end
end
diff --git a/lib/gitlab/github_import/importer/issue_event_importer.rb b/lib/gitlab/github_import/importer/issue_event_importer.rb
index ef456e56ee1..80749aae93c 100644
--- a/lib/gitlab/github_import/importer/issue_event_importer.rb
+++ b/lib/gitlab/github_import/importer/issue_event_importer.rb
@@ -15,11 +15,7 @@ module Gitlab
@client = client
end
- # TODO: Add MergeRequest events support
- # https://gitlab.com/groups/gitlab-org/-/epics/7673
def execute
- return if issue_event.issuable_type == 'MergeRequest'
-
importer = event_importer_class(issue_event)
if importer
importer.new(project, client).execute(issue_event)
@@ -49,6 +45,8 @@ module Gitlab
Gitlab::GithubImport::Importer::Events::CrossReferenced
when 'assigned', 'unassigned'
Gitlab::GithubImport::Importer::Events::ChangedAssignee
+ when 'review_requested', 'review_request_removed'
+ Gitlab::GithubImport::Importer::Events::ChangedReviewer
end
end
end
diff --git a/lib/gitlab/github_import/importer/protected_branch_importer.rb b/lib/gitlab/github_import/importer/protected_branch_importer.rb
new file mode 100644
index 00000000000..16215fdce8e
--- /dev/null
+++ b/lib/gitlab/github_import/importer/protected_branch_importer.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module Importer
+ class ProtectedBranchImporter
+ attr_reader :protected_branch, :project, :client
+
+ # protected_branch - An instance of
+ # `Gitlab::GithubImport::Representation::ProtectedBranch`.
+ # project - An instance of `Project`
+ # client - An instance of `Gitlab::GithubImport::Client`
+ def initialize(protected_branch, project, client)
+ @protected_branch = protected_branch
+ @project = project
+ @client = client
+ end
+
+ def execute
+ # The creator of the project is always allowed to create protected
+ # branches, so we skip the authorization check in this service class.
+ ProtectedBranches::CreateService
+ .new(project, project.creator, params)
+ .execute(skip_authorization: true)
+ end
+
+ private
+
+ def params
+ {
+ name: protected_branch.id,
+ push_access_levels_attributes: [{ access_level: Gitlab::Access::MAINTAINER }],
+ merge_access_levels_attributes: [{ access_level: Gitlab::Access::MAINTAINER }],
+ allow_force_push: allow_force_push?
+ }
+ end
+
+ def allow_force_push?
+ if ProtectedBranch.protected?(project, protected_branch.id)
+ ProtectedBranch.allow_force_push?(project, protected_branch.id) && protected_branch.allow_force_pushes
+ else
+ protected_branch.allow_force_pushes
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/importer/protected_branches_importer.rb b/lib/gitlab/github_import/importer/protected_branches_importer.rb
new file mode 100644
index 00000000000..b5be823d5ab
--- /dev/null
+++ b/lib/gitlab/github_import/importer/protected_branches_importer.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module Importer
+ class ProtectedBranchesImporter
+ include ParallelScheduling
+
+ # The method that will be called for traversing through all the objects to
+ # import, yielding them to the supplied block.
+ def each_object_to_import
+ repo = project.import_source
+
+ protected_branches = client.branches(repo).select { |branch| branch.protection&.enabled }
+ protected_branches.each do |protected_branch|
+ object = client.branch_protection(repo, protected_branch.name)
+ next if object.nil? || already_imported?(object)
+
+ yield object
+
+ Gitlab::GithubImport::ObjectCounter.increment(project, object_type, :fetched)
+ mark_as_imported(object)
+ end
+ end
+
+ def importer_class
+ ProtectedBranchImporter
+ end
+
+ def representation_class
+ Gitlab::GithubImport::Representation::ProtectedBranch
+ end
+
+ def sidekiq_worker_class
+ ImportProtectedBranchWorker
+ end
+
+ def object_type
+ :protected_branch
+ end
+
+ def collection_method
+ :protected_branches
+ end
+
+ def id_for_already_imported_cache(protected_branch)
+ protected_branch.name
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/importer/release_attachments_importer.rb b/lib/gitlab/github_import/importer/release_attachments_importer.rb
new file mode 100644
index 00000000000..6419851623c
--- /dev/null
+++ b/lib/gitlab/github_import/importer/release_attachments_importer.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module Importer
+ class ReleaseAttachmentsImporter
+ attr_reader :release_db_id, :release_description, :project
+
+ # release - An instance of `ReleaseAttachments`.
+ # project - An instance of `Project`.
+ # client - An instance of `Gitlab::GithubImport::Client`.
+ def initialize(release_attachments, project, _client = nil)
+ @release_db_id = release_attachments.release_db_id
+ @release_description = release_attachments.description
+ @project = project
+ end
+
+ def execute
+ attachment_urls = MarkdownText.fetch_attachment_urls(release_description)
+ new_description = attachment_urls.reduce(release_description) do |description, url|
+ new_url = download_attachment(url)
+ description.gsub(url, new_url)
+ end
+
+ Release.find(release_db_id).update_column(:description, new_description)
+ end
+
+ private
+
+ # in: github attachment markdown url
+ # out: gitlab attachment markdown url
+ def download_attachment(markdown_url)
+ url = extract_url_from_markdown(markdown_url)
+ name_prefix = extract_name_from_markdown(markdown_url)
+
+ downloader = ::Gitlab::GithubImport::AttachmentsDownloader.new(url)
+ file = downloader.perform
+ uploader = UploadService.new(project, file, FileUploader).execute
+ "#{name_prefix}(#{uploader.to_h[:url]})"
+ ensure
+ downloader&.delete
+ end
+
+ # in: "![image-icon](https://user-images.githubusercontent.com/..)"
+ # out: https://user-images.githubusercontent.com/..
+ def extract_url_from_markdown(text)
+ text.match(%r{https://.*\)$}).to_a[0].chop
+ end
+
+ # in: "![image-icon](https://user-images.githubusercontent.com/..)"
+ # out: ![image-icon]
+ def extract_name_from_markdown(text)
+ text.match(%r{^!?\[.*\]}).to_a[0]
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/importer/releases_attachments_importer.rb b/lib/gitlab/github_import/importer/releases_attachments_importer.rb
new file mode 100644
index 00000000000..7221c802d83
--- /dev/null
+++ b/lib/gitlab/github_import/importer/releases_attachments_importer.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module Importer
+ class ReleasesAttachmentsImporter
+ include ParallelScheduling
+
+ BATCH_SIZE = 100
+
+ # The method that will be called for traversing through all the objects to
+ # import, yielding them to the supplied block.
+ def each_object_to_import
+ project.releases.select(:id, :description).each_batch(of: BATCH_SIZE, column: :id) do |batch|
+ batch.each do |release|
+ next if already_imported?(release)
+
+ Gitlab::GithubImport::ObjectCounter.increment(project, object_type, :fetched)
+
+ yield release
+
+ # We mark the object as imported immediately so we don't end up
+ # scheduling it multiple times.
+ mark_as_imported(release)
+ end
+ end
+ end
+
+ def representation_class
+ Representation::ReleaseAttachments
+ end
+
+ def importer_class
+ ReleaseAttachmentsImporter
+ end
+
+ def sidekiq_worker_class
+ ImportReleaseAttachmentsWorker
+ end
+
+ def collection_method
+ :release_attachments
+ end
+
+ def object_type
+ :release_attachment
+ end
+
+ def id_for_already_imported_cache(release)
+ release.id
+ end
+
+ def object_representation(object)
+ representation_class.from_db_record(object)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/importer/repository_importer.rb b/lib/gitlab/github_import/importer/repository_importer.rb
index aba4729e9c8..708768a60cf 100644
--- a/lib/gitlab/github_import/importer/repository_importer.rb
+++ b/lib/gitlab/github_import/importer/repository_importer.rb
@@ -17,7 +17,7 @@ module Gitlab
# Returns true if we should import the wiki for the project.
# rubocop: disable CodeReuse/ActiveRecord
def import_wiki?
- client_repository&.has_wiki &&
+ client_repository[:has_wiki] &&
!project.wiki_repository_exists? &&
Gitlab::GitalyClient::RemoteService.exists?(wiki_url)
end
@@ -86,7 +86,7 @@ module Gitlab
private
def default_branch
- client_repository&.default_branch
+ client_repository[:default_branch]
end
def client_repository
diff --git a/lib/gitlab/github_import/importer/single_endpoint_issue_events_importer.rb b/lib/gitlab/github_import/importer/single_endpoint_issue_events_importer.rb
index 8e4015acbbc..8a9ddfc6ec0 100644
--- a/lib/gitlab/github_import/importer/single_endpoint_issue_events_importer.rb
+++ b/lib/gitlab/github_import/importer/single_endpoint_issue_events_importer.rb
@@ -7,7 +7,7 @@ module Gitlab
include ParallelScheduling
include SingleEndpointNotesImporting
- PROCESSED_PAGE_CACHE_KEY = 'issues/%{issue_iid}/%{collection}'
+ PROCESSED_PAGE_CACHE_KEY = 'issues/%{issuable_iid}/%{collection}'
BATCH_SIZE = 100
def initialize(project, client, parallel: true)
@@ -27,12 +27,20 @@ module Gitlab
Gitlab::GithubImport::ObjectCounter.increment(project, object_type, :fetched)
- associated.issue = { 'number' => parent_record.iid }
+ pull_request = parent_record.is_a? MergeRequest
+ associated.issue = { 'number' => parent_record.iid, 'pull_request' => pull_request }
yield(associated)
mark_as_imported(associated)
end
+ # In Github Issues and MergeRequests uses the same API to get their events.
+ # Even more - they have commonly uniq iid
+ def each_associated_page(&block)
+ issues_collection.each_batch(of: BATCH_SIZE, column: :iid) { |batch| process_batch(batch, &block) }
+ merge_requests_collection.each_batch(of: BATCH_SIZE, column: :iid) { |batch| process_batch(batch, &block) }
+ end
+
def importer_class
IssueEventImporter
end
@@ -53,16 +61,20 @@ module Gitlab
:issue_timeline
end
- def parent_collection
+ def issues_collection
project.issues.where.not(iid: already_imported_parents).select(:id, :iid) # rubocop: disable CodeReuse/ActiveRecord
end
+ def merge_requests_collection
+ project.merge_requests.where.not(iid: already_imported_parents).select(:id, :iid) # rubocop: disable CodeReuse/ActiveRecord
+ end
+
def parent_imported_cache_key
"github-importer/issues/#{collection_method}/already-imported/#{project.id}"
end
- def page_counter_id(issue)
- PROCESSED_PAGE_CACHE_KEY % { issue_iid: issue.iid, collection: collection_method }
+ def page_counter_id(issuable)
+ PROCESSED_PAGE_CACHE_KEY % { issuable_iid: issuable.iid, collection: collection_method }
end
def id_for_already_imported_cache(event)
@@ -74,10 +86,10 @@ module Gitlab
end
# Cross-referenced events on Github doesn't have id.
- def compose_associated_id!(issue, event)
+ def compose_associated_id!(issuable, event)
return if event.event != 'cross-referenced'
- event.id = "cross-reference##{issue.id}-in-#{event.source.issue.id}"
+ event.id = "cross-reference##{issuable.iid}-in-#{event.source.issue.id}"
end
end
end
diff --git a/lib/gitlab/github_import/markdown_text.rb b/lib/gitlab/github_import/markdown_text.rb
index 692016bd005..bf2856bc77f 100644
--- a/lib/gitlab/github_import/markdown_text.rb
+++ b/lib/gitlab/github_import/markdown_text.rb
@@ -1,5 +1,8 @@
# frozen_string_literal: true
+# This class includes overriding Kernel#format method
+# what makes impossible to use it here
+# rubocop:disable Style/FormatString
module Gitlab
module GithubImport
class MarkdownText
@@ -8,6 +11,21 @@ module Gitlab
ISSUE_REF_MATCHER = '%{github_url}/%{import_source}/issues'
PULL_REF_MATCHER = '%{github_url}/%{import_source}/pull'
+ MEDIA_TYPES = %w[gif jpeg jpg mov mp4 png svg webm].freeze
+ DOC_TYPES = %w[
+ csv docx fodg fodp fods fodt gz log md odf odg odp ods
+ odt pdf pptx tgz txt xls xlsx zip
+ ].freeze
+ ALL_TYPES = (MEDIA_TYPES + DOC_TYPES).freeze
+
+ # On github.com we have base url for docs and CDN url for media.
+ # On github EE as far as we can know there is no CDN urls and media is placed on base url.
+ # To no escape the escaping symbol we use single quotes instead of double with interpolation.
+ # rubocop:disable Style/StringConcatenation
+ CDN_URL_MATCHER = '(!\[.+\]\(%{github_media_cdn}/\d+/(\w|-)+\.(' + MEDIA_TYPES.join('|') + ')\))'
+ BASE_URL_MATCHER = '(\[.+\]\(%{github_url}/.+/.+/files/\d+/.+\.(' + ALL_TYPES.join('|') + ')\))'
+ # rubocop:enable Style/StringConcatenation
+
class << self
def format(*args)
new(*args).to_s
@@ -24,8 +42,20 @@ module Gitlab
.gsub(pull_ref_matcher, url_helpers.project_merge_requests_url(project))
end
+ def fetch_attachment_urls(text)
+ cdn_url_matcher = CDN_URL_MATCHER % { github_media_cdn: Regexp.escape(github_media_cdn) }
+ doc_url_matcher = BASE_URL_MATCHER % { github_url: Regexp.escape(github_url) }
+
+ text.scan(Regexp.new(cdn_url_matcher)).map(&:first) +
+ text.scan(Regexp.new(doc_url_matcher)).map(&:first)
+ end
+
private
+ def github_media_cdn
+ 'https://user-images.githubusercontent.com'
+ end
+
# Returns github domain without slash in the end
def github_url
oauth_config = Gitlab::Auth::OAuth::Provider.config_for('github') || {}
@@ -63,3 +93,4 @@ module Gitlab
end
end
end
+# rubocop:enable Style/FormatString
diff --git a/lib/gitlab/github_import/parallel_scheduling.rb b/lib/gitlab/github_import/parallel_scheduling.rb
index a8c18c74d24..bf5046de36c 100644
--- a/lib/gitlab/github_import/parallel_scheduling.rb
+++ b/lib/gitlab/github_import/parallel_scheduling.rb
@@ -63,7 +63,7 @@ module Gitlab
# Imports all the objects in sequence in the current thread.
def sequential_import
each_object_to_import do |object|
- repr = representation_class.from_api_response(object, additional_object_data)
+ repr = object_representation(object)
importer_class.new(repr, project, client).execute
end
@@ -83,7 +83,7 @@ module Gitlab
import_arguments = []
each_object_to_import do |object|
- repr = representation_class.from_api_response(object, additional_object_data)
+ repr = object_representation(object)
import_arguments << [project.id, repr.to_hash, waiter.key]
@@ -210,6 +210,10 @@ module Gitlab
{}
end
+ def object_representation(object)
+ representation_class.from_api_response(object, additional_object_data)
+ end
+
def info(project_id, extra = {})
Logger.info(log_attributes(project_id, extra))
end
diff --git a/lib/gitlab/github_import/representation/expose_attribute.rb b/lib/gitlab/github_import/representation/expose_attribute.rb
index d2438ee8094..84de4d4798d 100644
--- a/lib/gitlab/github_import/representation/expose_attribute.rb
+++ b/lib/gitlab/github_import/representation/expose_attribute.rb
@@ -20,6 +20,10 @@ module Gitlab
end
end
end
+
+ def [](key)
+ respond_to?(key.to_sym) ? attributes[key] : nil
+ end
end
end
end
diff --git a/lib/gitlab/github_import/representation/issue_event.rb b/lib/gitlab/github_import/representation/issue_event.rb
index 67a5df73a97..89271a7dcd6 100644
--- a/lib/gitlab/github_import/representation/issue_event.rb
+++ b/lib/gitlab/github_import/representation/issue_event.rb
@@ -10,7 +10,8 @@ module Gitlab
attr_reader :attributes
expose_attribute :id, :actor, :event, :commit_id, :label_title, :old_title, :new_title,
- :milestone_title, :issue, :source, :assignee, :assigner, :created_at
+ :milestone_title, :issue, :source, :assignee, :review_requester,
+ :requested_reviewer, :created_at
# attributes - A Hash containing the event details. The keys of this
# Hash (and any nested hashes) must be symbols.
@@ -47,7 +48,8 @@ module Gitlab
issue: event.issue&.to_h&.symbolize_keys,
source: event.source,
assignee: user_representation(event.assignee),
- assigner: user_representation(event.assigner),
+ requested_reviewer: user_representation(event.requested_reviewer),
+ review_requester: user_representation(event.review_requester),
created_at: event.created_at
)
end
@@ -57,7 +59,8 @@ module Gitlab
hash = Representation.symbolize_hash(raw_hash)
hash[:actor] = user_representation(hash[:actor], source: :hash)
hash[:assignee] = user_representation(hash[:assignee], source: :hash)
- hash[:assigner] = user_representation(hash[:assigner], source: :hash)
+ hash[:requested_reviewer] = user_representation(hash[:requested_reviewer], source: :hash)
+ hash[:review_requester] = user_representation(hash[:review_requester], source: :hash)
new(hash)
end
diff --git a/lib/gitlab/github_import/representation/protected_branch.rb b/lib/gitlab/github_import/representation/protected_branch.rb
new file mode 100644
index 00000000000..b80b7cf1076
--- /dev/null
+++ b/lib/gitlab/github_import/representation/protected_branch.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module Representation
+ class ProtectedBranch
+ include ToHash
+ include ExposeAttribute
+
+ attr_reader :attributes
+
+ expose_attribute :id, :allow_force_pushes
+
+ # Builds a Branch Protection info from a GitHub API response.
+ # Resource structure details:
+ # https://docs.github.com/en/rest/branches/branch-protection#get-branch-protection
+ # branch_protection - An instance of `Sawyer::Resource` containing the protection details.
+ def self.from_api_response(branch_protection, _additional_object_data = {})
+ branch_name = branch_protection.url.match(%r{/branches/(\S{1,255})/protection$})[1]
+
+ hash = {
+ id: branch_name,
+ allow_force_pushes: branch_protection.allow_force_pushes.enabled
+ }
+
+ new(hash)
+ end
+
+ # Builds a new Protection using a Hash that was built from a JSON payload.
+ def self.from_json_hash(raw_hash)
+ new(Representation.symbolize_hash(raw_hash))
+ end
+
+ # attributes - A Hash containing the raw Protection details. The keys of this
+ # Hash (and any nested hashes) must be symbols.
+ def initialize(attributes)
+ @attributes = attributes
+ end
+
+ def github_identifiers
+ { id: id }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/representation/release_attachments.rb b/lib/gitlab/github_import/representation/release_attachments.rb
new file mode 100644
index 00000000000..fd272be2405
--- /dev/null
+++ b/lib/gitlab/github_import/representation/release_attachments.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+# This class only partly represents Release record from DB and
+# is used to connect ReleasesAttachmentsImporter with ReleaseAttachmentsImporter
+# without modifying ObjectImporter a lot.
+# Attachments are inside release's `description`.
+module Gitlab
+ module GithubImport
+ module Representation
+ class ReleaseAttachments
+ include ToHash
+ include ExposeAttribute
+
+ attr_reader :attributes
+
+ expose_attribute :release_db_id, :description
+
+ # Builds a event from a GitHub API response.
+ #
+ # release - An instance of `Release` model.
+ def self.from_db_record(release)
+ new(
+ release_db_id: release.id,
+ description: release.description
+ )
+ end
+
+ def self.from_json_hash(raw_hash)
+ new Representation.symbolize_hash(raw_hash)
+ end
+
+ # attributes - A Hash containing the event details. The keys of this
+ # Hash (and any nested hashes) must be symbols.
+ def initialize(attributes)
+ @attributes = attributes
+ end
+
+ def github_identifiers
+ { db_id: release_db_id }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/sequential_importer.rb b/lib/gitlab/github_import/sequential_importer.rb
index 6bc37337799..ab37bc92ee7 100644
--- a/lib/gitlab/github_import/sequential_importer.rb
+++ b/lib/gitlab/github_import/sequential_importer.rb
@@ -16,6 +16,7 @@ module Gitlab
].freeze
PARALLEL_IMPORTERS = [
+ Importer::ProtectedBranchesImporter,
Importer::PullRequestsImporter,
Importer::IssuesImporter,
Importer::DiffNotesImporter,
diff --git a/lib/gitlab/github_import/single_endpoint_notes_importing.rb b/lib/gitlab/github_import/single_endpoint_notes_importing.rb
index 0a3559adde3..aea4059dfbc 100644
--- a/lib/gitlab/github_import/single_endpoint_notes_importing.rb
+++ b/lib/gitlab/github_import/single_endpoint_notes_importing.rb
@@ -63,23 +63,27 @@ module Gitlab
mark_as_imported(associated)
end
- def each_associated_page
+ def each_associated_page(&block)
parent_collection.each_batch(of: BATCH_SIZE, column: :iid) do |batch|
- batch.each do |parent_record|
- # The page counter needs to be scoped by parent_record to avoid skipping
- # pages of notes from already imported parent_record.
- page_counter = PageCounter.new(project, page_counter_id(parent_record))
- repo = project.import_source
- options = collection_options.merge(page: page_counter.current)
+ process_batch(batch, &block)
+ end
+ end
- client.each_page(collection_method, repo, parent_record.iid, options) do |page|
- next unless page_counter.set(page.number)
+ def process_batch(batch)
+ batch.each do |parent_record|
+ # The page counter needs to be scoped by parent_record to avoid skipping
+ # pages of notes from already imported parent_record.
+ page_counter = PageCounter.new(project, page_counter_id(parent_record))
+ repo = project.import_source
+ options = collection_options.merge(page: page_counter.current)
- yield parent_record, page
- end
+ client.each_page(collection_method, repo, parent_record.iid, options) do |page|
+ next unless page_counter.set(page.number)
- mark_parent_imported(parent_record)
+ yield parent_record, page
end
+
+ mark_parent_imported(parent_record)
end
end
diff --git a/lib/gitlab/github_import/user_finder.rb b/lib/gitlab/github_import/user_finder.rb
index 6d6a00d260d..1feb0d450f0 100644
--- a/lib/gitlab/github_import/user_finder.rb
+++ b/lib/gitlab/github_import/user_finder.rb
@@ -45,8 +45,10 @@ module Gitlab
object&.actor
when :assignee
object&.assignee
- when :assigner
- object&.assigner
+ when :requested_reviewer
+ object&.requested_reviewer
+ when :review_requester
+ object&.review_requester
else
object&.author
end
diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb
index 5f1802e323c..08a614edb4b 100644
--- a/lib/gitlab/gon_helper.rb
+++ b/lib/gitlab/gon_helper.rb
@@ -56,7 +56,6 @@ module Gitlab
push_frontend_feature_flag(:new_header_search)
push_frontend_feature_flag(:source_editor_toolbar)
push_frontend_feature_flag(:gl_avatar_for_all_user_avatars)
- push_frontend_feature_flag(:mr_attention_requests, current_user)
end
# Exposes the state of a feature flag to the frontend code.
diff --git a/lib/gitlab/graphql/errors.rb b/lib/gitlab/graphql/errors.rb
index 40b90310e8b..657364abfdf 100644
--- a/lib/gitlab/graphql/errors.rb
+++ b/lib/gitlab/graphql/errors.rb
@@ -7,6 +7,7 @@ module Gitlab
ArgumentError = Class.new(BaseError)
ResourceNotAvailable = Class.new(BaseError)
MutationError = Class.new(BaseError)
+ LimitError = Class.new(BaseError)
end
end
end
diff --git a/lib/gitlab/graphql/limit/field_call_count.rb b/lib/gitlab/graphql/limit/field_call_count.rb
new file mode 100644
index 00000000000..4165970a2a6
--- /dev/null
+++ b/lib/gitlab/graphql/limit/field_call_count.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Graphql
+ module Limit
+ class FieldCallCount < ::GraphQL::Schema::FieldExtension
+ def resolve(object:, arguments:, context:)
+ raise Gitlab::Graphql::Errors::ArgumentError, 'Limit must be specified.' unless limit
+ raise Gitlab::Graphql::Errors::LimitError, error_message if increment_call_count(context) > limit
+
+ yield(object, arguments)
+ end
+
+ private
+
+ def increment_call_count(context)
+ context[:call_count] ||= {}
+ context[:call_count][field] ||= 0
+ context[:call_count][field] += 1
+ end
+
+ def limit
+ options[:limit]
+ end
+
+ def error_message
+ "\"#{field.graphql_name}\" field can be requested only for #{limit} #{field.owner.graphql_name}(s) at a time."
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/graphql/pagination/keyset/connection.rb b/lib/gitlab/graphql/pagination/keyset/connection.rb
index b074c273996..987a5e7b74b 100644
--- a/lib/gitlab/graphql/pagination/keyset/connection.rb
+++ b/lib/gitlab/graphql/pagination/keyset/connection.rb
@@ -59,11 +59,15 @@ module Gitlab
if before
true
elsif first
- case sliced_nodes
- when Array
- sliced_nodes.size > limit_value
+ if Feature.enabled?(:graphql_keyset_pagination_without_next_page_query)
+ limited_nodes.size > limit_value
else
- sliced_nodes.limit(1).offset(limit_value).exists? # rubocop: disable CodeReuse/ActiveRecord
+ case sliced_nodes
+ when Array
+ sliced_nodes.size > limit_value
+ else
+ sliced_nodes.limit(1).offset(limit_value).exists? # rubocop: disable CodeReuse/ActiveRecord
+ end
end
else
false
@@ -89,7 +93,7 @@ module Gitlab
# So we're ok loading them into memory here as that's bound to happen
# anyway. Having them ready means we can modify the result while
# rendering the fields.
- @nodes ||= limited_nodes.to_a
+ @nodes ||= limited_nodes.to_a.take(limit_value) # rubocop: disable CodeReuse/ActiveRecord
end
def items
@@ -116,15 +120,21 @@ module Gitlab
end
if last
- paginated_nodes = LastItems.take_items(sliced_nodes, limit_value + 1)
+ paginated_nodes = sliced_nodes.last(limit_value + 1)
# there is an extra node, so there is a previous page
@has_previous_page = paginated_nodes.count > limit_value
@has_previous_page ? paginated_nodes.last(limit_value) : paginated_nodes
elsif loaded?(sliced_nodes)
- sliced_nodes.take(limit_value) # rubocop: disable CodeReuse/ActiveRecord
+ if Feature.enabled?(:graphql_keyset_pagination_without_next_page_query)
+ sliced_nodes.take(limit_value + 1) # rubocop: disable CodeReuse/ActiveRecord
+ else
+ sliced_nodes.take(limit_value) # rubocop: disable CodeReuse/ActiveRecord
+ end
+ elsif Feature.enabled?(:graphql_keyset_pagination_without_next_page_query)
+ sliced_nodes.limit(limit_value + 1).to_a
else
- sliced_nodes.limit(limit_value) # rubocop: disable CodeReuse/ActiveRecord
+ sliced_nodes.limit(limit_value)
end
end
end
@@ -141,7 +151,7 @@ module Gitlab
def limit_value
# note: only first _or_ last can be specified, not both
- @limit_value ||= [first, last, max_page_size].compact.min
+ @limit_value ||= [first, last, max_page_size, GitlabSchema.default_max_page_size].compact.min
end
def loaded?(items)
diff --git a/lib/gitlab/graphql/pagination/keyset/last_items.rb b/lib/gitlab/graphql/pagination/keyset/last_items.rb
deleted file mode 100644
index 960567a6fbc..00000000000
--- a/lib/gitlab/graphql/pagination/keyset/last_items.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Graphql
- module Pagination
- module Keyset
- # This class handles the last(N) ActiveRecord call even if a special ORDER BY configuration is present.
- # For the last(N) call, ActiveRecord calls reverse_order, however for some cases it raises
- # ActiveRecord::IrreversibleOrderError error.
- class LastItems
- # rubocop: disable CodeReuse/ActiveRecord
- def self.take_items(scope, count)
- if Gitlab::Pagination::Keyset::Order.keyset_aware?(scope)
- order = Gitlab::Pagination::Keyset::Order.extract_keyset_order_object(scope)
- items = scope.reorder(order.reversed_order).first(count)
- items.is_a?(Array) ? items.reverse : items
- else
- scope.last(count)
- end
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/graphql/type_name_deprecations.rb b/lib/gitlab/graphql/type_name_deprecations.rb
index c27ad1d54f5..1ec6fd1c09f 100644
--- a/lib/gitlab/graphql/type_name_deprecations.rb
+++ b/lib/gitlab/graphql/type_name_deprecations.rb
@@ -14,6 +14,9 @@ module Gitlab
DEPRECATIONS = [
Gitlab::Graphql::DeprecationsBase::NameDeprecation.new(
old_name: 'CiRunnerUpgradeStatusType', new_name: 'CiRunnerUpgradeStatus', milestone: '15.3'
+ ),
+ Gitlab::Graphql::DeprecationsBase::NameDeprecation.new(
+ old_name: 'RunnerMembershipFilter', new_name: 'CiRunnerMembershipFilter', milestone: '15.4'
)
].freeze
diff --git a/lib/gitlab/harbor/query.rb b/lib/gitlab/harbor/query.rb
index c120810ecf1..fcd984b01ce 100644
--- a/lib/gitlab/harbor/query.rb
+++ b/lib/gitlab/harbor/query.rb
@@ -17,7 +17,7 @@ module Gitlab
message: 'Id invalid'
}, allow_blank: true
validates :artifact_id, format: {
- with: /\A[a-zA-Z0-9\_\.\-$]+\z/,
+ with: /\A[a-zA-Z0-9\_\.\-$:]+\z/,
message: 'Id invalid'
}, allow_blank: true
validates :sort, format: {
diff --git a/lib/gitlab/health_checks/gitaly_check.rb b/lib/gitlab/health_checks/gitaly_check.rb
index f5f142c251f..2bd8ea711b5 100644
--- a/lib/gitlab/health_checks/gitaly_check.rb
+++ b/lib/gitlab/health_checks/gitaly_check.rb
@@ -27,17 +27,35 @@ module Gitlab
end
def check(storage_name)
- serv = Gitlab::GitalyClient::HealthCheckService.new(storage_name)
- result = serv.check
+ storage_healthy = healthy(storage_name)
+ unless storage_healthy[:success]
+ return HealthChecks::Result.new(
+ name,
+ storage_healthy[:success],
+ storage_healthy[:message],
+ shard: storage_name
+ )
+ end
+ storage_ready = ready(storage_name)
HealthChecks::Result.new(
name,
- result[:success],
- result[:message],
+ storage_ready[:success],
+ storage_ready[:message],
shard: storage_name
)
end
+ def healthy(storage_name)
+ serv = Gitlab::GitalyClient::HealthCheckService.new(storage_name)
+ serv.check
+ end
+
+ def ready(storage_name)
+ serv = Gitlab::GitalyClient::ServerService.new(storage_name)
+ serv.readiness_check
+ end
+
private
def metric_prefix
diff --git a/lib/gitlab/health_checks/redis.rb b/lib/gitlab/health_checks/redis.rb
new file mode 100644
index 00000000000..895bce5a5a9
--- /dev/null
+++ b/lib/gitlab/health_checks/redis.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module HealthChecks
+ module Redis
+ ALL_INSTANCE_CHECKS =
+ ::Gitlab::Redis::ALL_CLASSES.map do |instance_class|
+ check_class = Class.new
+ check_class.extend(RedisAbstractCheck)
+ const_set("#{instance_class.store_name}Check", check_class)
+
+ check_class
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/health_checks/redis/cache_check.rb b/lib/gitlab/health_checks/redis/cache_check.rb
deleted file mode 100644
index bd843bdaac4..00000000000
--- a/lib/gitlab/health_checks/redis/cache_check.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module HealthChecks
- module Redis
- class CacheCheck
- extend RedisAbstractCheck
- end
- end
- end
-end
diff --git a/lib/gitlab/health_checks/redis/queues_check.rb b/lib/gitlab/health_checks/redis/queues_check.rb
deleted file mode 100644
index fb92db937dc..00000000000
--- a/lib/gitlab/health_checks/redis/queues_check.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module HealthChecks
- module Redis
- class QueuesCheck
- extend RedisAbstractCheck
- end
- end
- end
-end
diff --git a/lib/gitlab/health_checks/redis/rate_limiting_check.rb b/lib/gitlab/health_checks/redis/rate_limiting_check.rb
deleted file mode 100644
index 0e9d94f7dff..00000000000
--- a/lib/gitlab/health_checks/redis/rate_limiting_check.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module HealthChecks
- module Redis
- class RateLimitingCheck
- extend RedisAbstractCheck
- end
- end
- end
-end
diff --git a/lib/gitlab/health_checks/redis/redis_abstract_check.rb b/lib/gitlab/health_checks/redis/redis_abstract_check.rb
index ecad4b06ea9..9a9a4d1faba 100644
--- a/lib/gitlab/health_checks/redis/redis_abstract_check.rb
+++ b/lib/gitlab/health_checks/redis/redis_abstract_check.rb
@@ -10,12 +10,12 @@ module Gitlab
successful?(check)
end
- private
-
def redis_instance_class_name
Gitlab::Redis.const_get(redis_instance_name.camelize, false)
end
+ private
+
def metric_prefix
"redis_#{redis_instance_name}_ping"
end
diff --git a/lib/gitlab/health_checks/redis/redis_check.rb b/lib/gitlab/health_checks/redis/redis_check.rb
deleted file mode 100644
index c793a939abd..00000000000
--- a/lib/gitlab/health_checks/redis/redis_check.rb
+++ /dev/null
@@ -1,38 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module HealthChecks
- module Redis
- class RedisCheck
- extend SimpleAbstractCheck
-
- class << self
- private
-
- def metric_prefix
- 'redis_ping'
- end
-
- def successful?(result)
- result == true
- end
-
- def check
- redis_health_checks.all?(&:check_up)
- end
-
- def redis_health_checks
- [
- Gitlab::HealthChecks::Redis::CacheCheck,
- Gitlab::HealthChecks::Redis::QueuesCheck,
- Gitlab::HealthChecks::Redis::SharedStateCheck,
- Gitlab::HealthChecks::Redis::TraceChunksCheck,
- Gitlab::HealthChecks::Redis::RateLimitingCheck,
- Gitlab::HealthChecks::Redis::SessionsCheck
- ]
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/health_checks/redis/sessions_check.rb b/lib/gitlab/health_checks/redis/sessions_check.rb
deleted file mode 100644
index 90a4c868f40..00000000000
--- a/lib/gitlab/health_checks/redis/sessions_check.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module HealthChecks
- module Redis
- class SessionsCheck
- extend RedisAbstractCheck
- end
- end
- end
-end
diff --git a/lib/gitlab/health_checks/redis/shared_state_check.rb b/lib/gitlab/health_checks/redis/shared_state_check.rb
deleted file mode 100644
index 80f91784b8c..00000000000
--- a/lib/gitlab/health_checks/redis/shared_state_check.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module HealthChecks
- module Redis
- class SharedStateCheck
- extend RedisAbstractCheck
- end
- end
- end
-end
diff --git a/lib/gitlab/health_checks/redis/trace_chunks_check.rb b/lib/gitlab/health_checks/redis/trace_chunks_check.rb
deleted file mode 100644
index 9a89a1ce51d..00000000000
--- a/lib/gitlab/health_checks/redis/trace_chunks_check.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module HealthChecks
- module Redis
- class TraceChunksCheck
- extend RedisAbstractCheck
- end
- end
- end
-end
diff --git a/lib/gitlab/hook_data/project_member_builder.rb b/lib/gitlab/hook_data/project_member_builder.rb
index 90fc83fdf21..2ee61705ec1 100644
--- a/lib/gitlab/hook_data/project_member_builder.rb
+++ b/lib/gitlab/hook_data/project_member_builder.rb
@@ -37,16 +37,16 @@ module Gitlab
project = project_member.project || Project.unscoped.find(project_member.source_id)
{
- project_name: project.name,
- project_path: project.path,
- project_path_with_namespace: project.full_path,
- project_id: project.id,
- user_username: project_member.user.username,
- user_name: project_member.user.name,
- user_email: project_member.user.email,
- user_id: project_member.user.id,
- access_level: project_member.human_access,
- project_visibility: project.visibility
+ project_name: project.name,
+ project_path: project.path,
+ project_path_with_namespace: project.full_path,
+ project_id: project.id,
+ user_username: project_member.user.username,
+ user_name: project_member.user.name,
+ user_email: project_member.user.email,
+ user_id: project_member.user.id,
+ access_level: project_member.human_access,
+ project_visibility: project.visibility
}
end
diff --git a/lib/gitlab/i18n.rb b/lib/gitlab/i18n.rb
index 30465ff5f74..5b9216c0914 100644
--- a/lib/gitlab/i18n.rb
+++ b/lib/gitlab/i18n.rb
@@ -44,30 +44,30 @@ module Gitlab
TRANSLATION_LEVELS = {
'bg' => 0,
'cs_CZ' => 0,
- 'da_DK' => 39,
+ 'da_DK' => 38,
'de' => 17,
'en' => 100,
'eo' => 0,
- 'es' => 38,
+ 'es' => 37,
'fil_PH' => 0,
'fr' => 11,
'gl_ES' => 0,
'id_ID' => 0,
'it' => 1,
- 'ja' => 32,
- 'ko' => 12,
+ 'ja' => 31,
+ 'ko' => 17,
'nb_NO' => 26,
'nl_NL' => 0,
'pl_PL' => 4,
- 'pt_BR' => 55,
- 'ro_RO' => 100,
+ 'pt_BR' => 56,
+ 'ro_RO' => 99,
'ru' => 27,
'si_LK' => 10,
- 'tr_TR' => 12,
+ 'tr_TR' => 11,
'uk' => 50,
- 'zh_CN' => 99,
+ 'zh_CN' => 97,
'zh_HK' => 1,
- 'zh_TW' => 100
+ 'zh_TW' => 99
}.freeze
private_constant :TRANSLATION_LEVELS
diff --git a/lib/gitlab/import_export/attributes_finder.rb b/lib/gitlab/import_export/attributes_finder.rb
index 4abc3da1190..8843b4f5755 100644
--- a/lib/gitlab/import_export/attributes_finder.rb
+++ b/lib/gitlab/import_export/attributes_finder.rb
@@ -12,6 +12,7 @@ module Gitlab
@methods = config[:methods] || {}
@preloads = config[:preloads] || {}
@export_reorders = config[:export_reorders] || {}
+ @include_if_exportable = config[:include_if_exportable] || {}
end
def find_root(model_key)
@@ -35,7 +36,8 @@ module Gitlab
methods: @methods[model_key],
include: resolve_model_tree(model_tree),
preload: resolve_preloads(model_key, model_tree),
- export_reorder: @export_reorders[model_key]
+ export_reorder: @export_reorders[model_key],
+ include_if_exportable: @include_if_exportable[model_key]
}.compact
end
diff --git a/lib/gitlab/import_export/base/relation_factory.rb b/lib/gitlab/import_export/base/relation_factory.rb
index 1cbfcbdb595..bbec473d29d 100644
--- a/lib/gitlab/import_export/base/relation_factory.rb
+++ b/lib/gitlab/import_export/base/relation_factory.rb
@@ -31,6 +31,8 @@ module Gitlab
TOKEN_RESET_MODELS = %i[Project Namespace Group Ci::Trigger Ci::Build Ci::Runner ProjectHook ErrorTracking::ProjectErrorTrackingSetting].freeze
+ attr_reader :relation_name, :importable
+
def self.create(*args, **kwargs)
new(*args, **kwargs).create
end
diff --git a/lib/gitlab/import_export/base/relation_object_saver.rb b/lib/gitlab/import_export/base/relation_object_saver.rb
index ea989487ebd..3c473449ec0 100644
--- a/lib/gitlab/import_export/base/relation_object_saver.rb
+++ b/lib/gitlab/import_export/base/relation_object_saver.rb
@@ -58,8 +58,19 @@ module Gitlab
records.each_slice(BATCH_SIZE) do |batch|
valid_records, invalid_records = batch.partition { |record| record.valid? }
- invalid_subrelations << invalid_records
relation_object.public_send(relation_name) << valid_records
+
+ # Attempt to save some of the invalid subrelations, as they might be valid after all.
+ # For example, a merge request `Approval` validates presence of merge_request_id.
+ # It is not present at a time of calling `#valid?` above, since it's indeed missing.
+ # However, when saving such subrelation against already persisted merge request
+ # such validation won't fail (e.g. `merge_request.approvals << Approval.new(user_id: 1)`),
+ # as we're operating on a merge request that has `id` present.
+ invalid_records.each do |invalid_record|
+ relation_object.public_send(relation_name) << invalid_record
+
+ invalid_subrelations << invalid_record unless invalid_record.persisted?
+ end
end
end
end
diff --git a/lib/gitlab/import_export/group/import_export.yml b/lib/gitlab/import_export/group/import_export.yml
index 8df5d52bf77..a08efdf400b 100644
--- a/lib/gitlab/import_export/group/import_export.yml
+++ b/lib/gitlab/import_export/group/import_export.yml
@@ -27,6 +27,26 @@ included_attributes:
- :name
namespace_settings:
- :prevent_sharing_groups_outside_hierarchy
+ iterations_cadence: &iterations_cadence_definition
+ - :group_id
+ - :created_at
+ - :updated_at
+ - :start_date
+ - :active
+ - :roll_over
+ - :title
+ - :description
+ - :sequence
+ iterations_cadences: *iterations_cadence_definition
+ iteration: &iteration_definition
+ - :iid
+ - :created_at
+ - :updated_at
+ - :start_date
+ - :due_date
+ - :group_id
+ - :description
+ iterations: *iteration_definition
excluded_attributes:
group:
@@ -44,6 +64,23 @@ excluded_attributes:
- :max_pages_size
epics:
- :state_id
+ iterations_cadence: &iterations_cadence_definition
+ - :id
+ - :next_run_date
+ - :duration_in_weeks
+ - :iterations_in_advance
+ - :automatic
+ iterations_cadences: *iterations_cadence_definition
+ iteration: &iteration_excluded_definition
+ - :id
+ - :title
+ - :title_html
+ - :project_id
+ - :description_html
+ - :cached_markdown_version
+ - :iterations_cadence_id
+ - :sequence
+ iterations: *iteration_excluded_definition
methods:
labels:
@@ -83,6 +120,7 @@ ee:
- events:
- :push_event_payload
- :system_note_metadata
+ - :resource_state_events
- boards:
- :board_assignee
- :milestone
@@ -92,3 +130,5 @@ ee:
- milestone:
- events:
- :push_event_payload
+ - iterations_cadences:
+ - :iterations
diff --git a/lib/gitlab/import_export/group/legacy_import_export.yml b/lib/gitlab/import_export/group/legacy_import_export.yml
index 082d2346f3d..6507def7d01 100644
--- a/lib/gitlab/import_export/group/legacy_import_export.yml
+++ b/lib/gitlab/import_export/group/legacy_import_export.yml
@@ -24,6 +24,29 @@ included_attributes:
- :username
author:
- :name
+ iterations_cadence: &iterations_cadence_definition
+ - :group_id
+ - :created_at
+ - :updated_at
+ - :start_date
+ - :next_run_date
+ - :duration_in_weeks
+ - :iterations_in_advance
+ - :active
+ - :automatic
+ - :roll_over
+ - :title
+ - :description
+ iterations_cadences: *iterations_cadence_definition
+ iteration: &iteration_definition
+ - :iid
+ - :created_at
+ - :updated_at
+ - :start_date
+ - :due_date
+ - :group_id
+ - :description
+ iterations: *iteration_definition
excluded_attributes:
group:
@@ -41,6 +64,18 @@ excluded_attributes:
- :extra_shared_runners_minutes_limit
epics:
- :state_id
+ iterations_cadence: &iterations_cadence_definition
+ - :id
+ iterations_cadences: *iterations_cadence_definition
+ iteration: &iteration_excluded_definition
+ - :id
+ - :title
+ - :title_html
+ - :project_id
+ - :description_html
+ - :cached_markdown_version
+ - :iterations_cadence_id
+ iterations: *iteration_excluded_definition
methods:
labels:
@@ -79,6 +114,7 @@ ee:
- :award_emoji
- events:
- :push_event_payload
+ - :resource_state_events
- boards:
- :board_assignee
- :milestone
@@ -88,3 +124,5 @@ ee:
- milestone:
- events:
- :push_event_payload
+ - iterations_cadences:
+ - :iterations
diff --git a/lib/gitlab/import_export/group/legacy_tree_restorer.rb b/lib/gitlab/import_export/group/legacy_tree_restorer.rb
index 8b39362b6bb..fa9e765b33a 100644
--- a/lib/gitlab/import_export/group/legacy_tree_restorer.rb
+++ b/lib/gitlab/import_export/group/legacy_tree_restorer.rb
@@ -68,23 +68,23 @@ module Gitlab
def restorer
@relation_tree_restorer ||= RelationTreeRestorer.new(
- user: @user,
- shared: @shared,
- relation_reader: relation_reader,
- members_mapper: members_mapper,
- object_builder: object_builder,
- relation_factory: relation_factory,
- reader: reader,
- importable: @group,
+ user: @user,
+ shared: @shared,
+ relation_reader: relation_reader,
+ members_mapper: members_mapper,
+ object_builder: object_builder,
+ relation_factory: relation_factory,
+ reader: reader,
+ importable: @group,
importable_attributes: @group_attributes,
- importable_path: nil
+ importable_path: nil
)
end
def create_group(group_hash:, parent_group:)
group_params = {
- name: group_hash['name'],
- path: group_hash['path'],
+ name: group_hash['name'],
+ path: group_hash['path'],
parent_id: parent_group&.id,
visibility_level: sub_group_visibility_level(group_hash, parent_group)
}
diff --git a/lib/gitlab/import_export/group/relation_factory.rb b/lib/gitlab/import_export/group/relation_factory.rb
index 258078d595b..1b8436c4ed9 100644
--- a/lib/gitlab/import_export/group/relation_factory.rb
+++ b/lib/gitlab/import_export/group/relation_factory.rb
@@ -5,10 +5,11 @@ module Gitlab
module Group
class RelationFactory < Base::RelationFactory
OVERRIDES = {
- labels: :group_labels,
+ labels: :group_labels,
priorities: :label_priorities,
- label: :group_label,
- parent: :epic
+ label: :group_label,
+ parent: :epic,
+ iterations_cadences: 'Iterations::Cadence'
}.freeze
EXISTING_OBJECT_RELATIONS = %i[
@@ -25,7 +26,10 @@ module Gitlab
private
def setup_models
- setup_note if @relation_name == :notes
+ case @relation_name
+ when :notes then setup_note
+ when :'Iterations::Cadence' then setup_iterations_cadence
+ end
update_group_references
end
@@ -44,6 +48,10 @@ module Gitlab
def use_attributes_permitter?
false
end
+
+ def setup_iterations_cadence
+ @relation_hash['automatic'] = false
+ end
end
end
end
diff --git a/lib/gitlab/import_export/group/relation_tree_restorer.rb b/lib/gitlab/import_export/group/relation_tree_restorer.rb
index fab677bd772..5a78f2fb531 100644
--- a/lib/gitlab/import_export/group/relation_tree_restorer.rb
+++ b/lib/gitlab/import_export/group/relation_tree_restorer.rb
@@ -136,9 +136,9 @@ module Gitlab
attributes_permitter.permit(importable_class_sym, params)
else
Gitlab::ImportExport::AttributeCleaner.clean(
- relation_hash: params,
+ relation_hash: params,
relation_class: importable_class,
- excluded_keys: excluded_keys_for_relation(importable_class_sym))
+ excluded_keys: excluded_keys_for_relation(importable_class_sym))
end
end
diff --git a/lib/gitlab/import_export/group/tree_saver.rb b/lib/gitlab/import_export/group/tree_saver.rb
index 796b9258e57..b4c86c3fc7f 100644
--- a/lib/gitlab/import_export/group/tree_saver.rb
+++ b/lib/gitlab/import_export/group/tree_saver.rb
@@ -46,7 +46,8 @@ module Gitlab
group,
group_tree,
json_writer,
- exportable_path: "groups/#{group.id}"
+ exportable_path: "groups/#{group.id}",
+ current_user: @current_user
).execute
end
diff --git a/lib/gitlab/import_export/json/streaming_serializer.rb b/lib/gitlab/import_export/json/streaming_serializer.rb
index 78f43f79072..99396d64779 100644
--- a/lib/gitlab/import_export/json/streaming_serializer.rb
+++ b/lib/gitlab/import_export/json/streaming_serializer.rb
@@ -6,11 +6,7 @@ module Gitlab
class StreamingSerializer
include Gitlab::ImportExport::CommandLineUtil
- BATCH_SIZE = 2
-
- def self.batch_size(exportable)
- BATCH_SIZE
- end
+ BATCH_SIZE = 100
class Raw < String
def to_json(*_args)
@@ -18,8 +14,9 @@ module Gitlab
end
end
- def initialize(exportable, relations_schema, json_writer, exportable_path:, logger: Gitlab::Export::Logger)
+ def initialize(exportable, relations_schema, json_writer, current_user:, exportable_path:, logger: Gitlab::Export::Logger)
@exportable = exportable
+ @current_user = current_user
@exportable_path = exportable_path
@relations_schema = relations_schema
@json_writer = json_writer
@@ -63,7 +60,7 @@ module Gitlab
private
- attr_reader :json_writer, :relations_schema, :exportable, :logger
+ attr_reader :json_writer, :relations_schema, :exportable, :logger, :current_user
def serialize_many_relations(key, records, options)
log_relation_export(key, records.size)
@@ -77,7 +74,7 @@ module Gitlab
batch.each do |record|
before_read_callback(record)
- items << Raw.new(record.to_json(options))
+ items << exportable_json_record(record, options, key)
after_read_callback(record)
end
@@ -87,8 +84,29 @@ module Gitlab
json_writer.write_relation_array(@exportable_path, key, enumerator)
end
+ def exportable_json_record(record, options, key)
+ associations = relations_schema[:include_if_exportable]&.dig(key)
+ return Raw.new(record.to_json(options)) unless associations && options[:include]
+
+ filtered_options = options.deep_dup
+ associations.each do |association|
+ filtered_options[:include].delete_if do |option|
+ !exportable_json_association?(option, record, association.to_sym)
+ end
+ end
+
+ Raw.new(record.to_json(filtered_options))
+ end
+
+ def exportable_json_association?(option, record, association)
+ return true unless option.has_key?(association)
+ return false unless record.respond_to?(:exportable_association?)
+
+ record.exportable_association?(association, current_user: current_user)
+ end
+
def batch(relation, key)
- opts = { of: batch_size }
+ opts = { of: BATCH_SIZE }
order_by = reorders(relation, key)
# we need to sort issues by non primary key column(relative_position)
@@ -115,7 +133,7 @@ module Gitlab
enumerator = Enumerator.new do |items|
records.each do |record|
- items << Raw.new(record.to_json(options))
+ items << exportable_json_record(record, options, key)
end
end
@@ -125,7 +143,7 @@ module Gitlab
def serialize_single_relation(key, record, options)
log_relation_export(key)
- json = Raw.new(record.to_json(options))
+ json = exportable_json_record(record, options, key)
json_writer.write_relation(@exportable_path, key, json)
end
@@ -138,10 +156,6 @@ module Gitlab
relations_schema[:preload]
end
- def batch_size
- @batch_size ||= self.class.batch_size(@exportable)
- end
-
def reorders(relation, key)
export_reorder = relations_schema[:export_reorder]&.dig(key)
return unless export_reorder
diff --git a/lib/gitlab/import_export/legacy_relation_tree_saver.rb b/lib/gitlab/import_export/legacy_relation_tree_saver.rb
index c6b961ea210..cf75a2c7fa8 100644
--- a/lib/gitlab/import_export/legacy_relation_tree_saver.rb
+++ b/lib/gitlab/import_export/legacy_relation_tree_saver.rb
@@ -7,7 +7,7 @@ module Gitlab
def serialize(exportable, relations_tree)
Gitlab::ImportExport::FastHashSerializer
- .new(exportable, relations_tree, batch_size: batch_size(exportable))
+ .new(exportable, relations_tree)
.execute
end
@@ -18,12 +18,6 @@ module Gitlab
File.write(File.join(dir_path, filename), tree_json)
end
-
- private
-
- def batch_size(exportable)
- Gitlab::ImportExport::Json::StreamingSerializer.batch_size(exportable)
- end
end
end
end
diff --git a/lib/gitlab/import_export/members_mapper.rb b/lib/gitlab/import_export/members_mapper.rb
index b1f2a17d4b7..c94549a2b3f 100644
--- a/lib/gitlab/import_export/members_mapper.rb
+++ b/lib/gitlab/import_export/members_mapper.rb
@@ -139,7 +139,7 @@ module Gitlab
end
def parsed_hash(member)
- Gitlab::ImportExport::AttributeCleaner.clean(relation_hash: member.deep_stringify_keys,
+ Gitlab::ImportExport::AttributeCleaner.clean(relation_hash: member.deep_stringify_keys,
relation_class: relation_class)
end
diff --git a/lib/gitlab/import_export/project/import_export.yml b/lib/gitlab/import_export/project/import_export.yml
index c5b8f3fd35b..33e4823f192 100644
--- a/lib/gitlab/import_export/project/import_export.yml
+++ b/lib/gitlab/import_export/project/import_export.yml
@@ -29,6 +29,9 @@ tree:
- resource_label_events:
- label:
- :priorities
+ - resource_milestone_events:
+ - :milestone
+ - :resource_state_events
- designs:
- notes:
- :author
@@ -82,6 +85,9 @@ tree:
- resource_label_events:
- label:
- :priorities
+ - resource_milestone_events:
+ - :milestone
+ - :resource_state_events
- :external_pull_requests
- ci_pipelines:
- notes:
@@ -287,6 +293,7 @@ included_attributes:
- :forking_access_level
- :metrics_dashboard_access_level
- :operations_access_level
+ - :monitor_access_level
- :analytics_access_level
- :security_and_compliance_access_level
- :container_registry_access_level
@@ -551,6 +558,7 @@ included_attributes:
- :failure_reason
- :scheduled_at
- :scheduling_type
+ - :ci_stage
ci_pipelines:
- :ref
- :sha
@@ -599,7 +607,6 @@ included_attributes:
merge_request_assignees:
- :user_id
- :created_at
- - :state
merge_request_reviewers:
- :user_id
- :created_at
@@ -699,6 +706,7 @@ included_attributes:
- :metrics_dashboard_access_level
- :analytics_access_level
- :operations_access_level
+ - :monitor_access_level
- :security_and_compliance_access_level
- :container_registry_access_level
- :package_registry_access_level
@@ -721,6 +729,18 @@ included_attributes:
- :build_git_strategy
- :build_enabled
- :security_and_compliance_enabled
+ resource_milestone_events:
+ - :user_id
+ - :action
+ - :created_at
+ - :state
+ resource_state_events:
+ - :user_id
+ - :state
+ - :created_at
+ - :source_commit
+ - :close_after_error_tracking_resolve
+ - :close_auto_resolve_prometheus_alert
# Do not include the following attributes for the models specified.
excluded_attributes:
@@ -989,6 +1009,46 @@ excluded_attributes:
milestone_releases:
- :milestone_id
- :release_id
+ resource_milestone_events:
+ - :id
+ - :issue_id
+ - :merge_request_id
+ - :milestone_id
+ resource_state_events:
+ - :id
+ - :issue_id
+ - :merge_request_id
+ - :epic_id
+ - :source_merge_request_id
+ iteration:
+ - :id
+ - :title
+ - :title_html
+ - :project_id
+ - :description_html
+ - :cached_markdown_version
+ - :iterations_cadence_id
+ - :sequence
+ resource_iteration_events:
+ - :id
+ - :issue_id
+ - :merge_request_id
+ - :iteration_id
+ iterations_cadence:
+ - :id
+ - :last_run_date
+ - :duration_in_weeks
+ - :iterations_in_advance
+ - :automatic
+ - :group_id
+ - :created_at
+ - :updated_at
+ - :start_date
+ - :active
+ - :roll_over
+ - :description
+ - :sequence
+
methods:
notes:
- :type
@@ -1062,6 +1122,11 @@ ee:
- epic_issue:
- :epic
- :issuable_sla
+ - iteration:
+ - :iterations_cadence
+ - resource_iteration_events:
+ - iteration:
+ - :iterations_cadence
- protected_branches:
- :unprotect_access_levels
- protected_environments:
@@ -1120,5 +1185,44 @@ ee:
- :auto_fix_dependency_scanning
- :auto_fix_sast
project:
- - :requirements_enabled
- - :requirements_access_level
+ - :requirements_enabled
+ - :requirements_access_level
+ resource_iteration_events:
+ - :user_id
+ - :action
+ - :created_at
+ iteration:
+ - :iid
+ - :created_at
+ - :updated_at
+ - :start_date
+ - :due_date
+ - :group_id
+ - :description
+ iterations_cadence:
+ - :title
+
+ preloads:
+ issues:
+ epic:
+
+ # When associated resources are from outside the project, you might need to
+ # validate that a user who is exporting the project or group can access these
+ # associations. `include_if_exportable` accepts an array of associations for a
+ # resource. During export, the `exportable_association?` method on the
+ # resource is called with the association's name and user to validate if
+ # associated resource can be included in the export.
+ #
+ # This definition will call issue's `exportable_association?(:epic_issue,
+ # current_user: current_user)` method and include issue's epic_issue association
+ # for each issue only if the method returns true:
+ #
+ # Example:
+ # include_if_exportable:
+ # project:
+ # issues:
+ # - epic_issue
+ include_if_exportable:
+ project:
+ issues:
+ - :epic_issue
diff --git a/lib/gitlab/import_export/project/import_task.rb b/lib/gitlab/import_export/project/import_task.rb
index 59bb8af750e..89f2b36ea58 100644
--- a/lib/gitlab/import_export/project/import_task.rb
+++ b/lib/gitlab/import_export/project/import_task.rb
@@ -80,8 +80,8 @@ module Gitlab
def import_params
{
namespace_id: namespace.id,
- path: project_path,
- file: File.open(file_path)
+ path: project_path,
+ file: File.open(file_path)
}
end
diff --git a/lib/gitlab/import_export/project/object_builder.rb b/lib/gitlab/import_export/project/object_builder.rb
index bf60d115a25..50a67a746f8 100644
--- a/lib/gitlab/import_export/project/object_builder.rb
+++ b/lib/gitlab/import_export/project/object_builder.rb
@@ -21,7 +21,7 @@ module Gitlab
end
def find
- return if epic? && group.nil?
+ return if group_relation_without_group?
return find_diff_commit_user if diff_commit_user?
return find_diff_commit if diff_commit?
@@ -60,7 +60,7 @@ module Gitlab
def prepare_attributes
attributes.dup.tap do |atts|
- atts.delete('group') unless epic?
+ atts.delete('group') unless epic? || iteration?
if label?
atts['type'] = 'ProjectLabel' # Always create project labels
@@ -141,6 +141,10 @@ module Gitlab
klass == MergeRequestDiffCommit
end
+ def iteration?
+ klass == Iteration
+ end
+
# If an existing group milestone used the IID
# claim the IID back and set the group milestone to use one available
# This is necessary to fix situations like the following:
@@ -157,7 +161,13 @@ module Gitlab
milestone.ensure_project_iid!
milestone.save!
end
+
+ def group_relation_without_group?
+ (epic? || iteration?) && group.nil?
+ end
end
end
end
end
+
+Gitlab::ImportExport::Project::ObjectBuilder.prepend_mod
diff --git a/lib/gitlab/import_export/project/relation_saver.rb b/lib/gitlab/import_export/project/relation_saver.rb
index b40827e36f8..8e91adac196 100644
--- a/lib/gitlab/import_export/project/relation_saver.rb
+++ b/lib/gitlab/import_export/project/relation_saver.rb
@@ -32,7 +32,8 @@ module Gitlab
project,
reader.project_tree,
json_writer,
- exportable_path: 'project'
+ exportable_path: 'project',
+ current_user: nil
)
end
diff --git a/lib/gitlab/import_export/project/relation_tree_restorer.rb b/lib/gitlab/import_export/project/relation_tree_restorer.rb
index 6e9548f393a..47196db6f8a 100644
--- a/lib/gitlab/import_export/project/relation_tree_restorer.rb
+++ b/lib/gitlab/import_export/project/relation_tree_restorer.rb
@@ -5,7 +5,7 @@ module Gitlab
module Project
class RelationTreeRestorer < ImportExport::Group::RelationTreeRestorer
# Relations which cannot be saved at project level (and have a group assigned)
- GROUP_MODELS = [GroupLabel, Milestone, Epic].freeze
+ GROUP_MODELS = [GroupLabel, Milestone, Epic, Iteration].freeze
private
diff --git a/lib/gitlab/import_export/project/tree_saver.rb b/lib/gitlab/import_export/project/tree_saver.rb
index 1b54e4b975e..bd34cd3ff6e 100644
--- a/lib/gitlab/import_export/project/tree_saver.rb
+++ b/lib/gitlab/import_export/project/tree_saver.rb
@@ -50,7 +50,8 @@ module Gitlab
reader.project_tree,
json_writer,
exportable_path: "project",
- logger: @logger
+ logger: @logger,
+ current_user: @current_user
)
Retriable.retriable(on: Net::OpenTimeout, on_retry: on_retry) do
diff --git a/lib/gitlab/instrumentation/redis.rb b/lib/gitlab/instrumentation/redis.rb
index 4fee779c767..a371930621d 100644
--- a/lib/gitlab/instrumentation/redis.rb
+++ b/lib/gitlab/instrumentation/redis.rb
@@ -4,15 +4,20 @@ module Gitlab
module Instrumentation
# Aggregates Redis measurements from different request storage sources.
class Redis
+ # Actioncable has it's separate instrumentation, but isn't configurable
+ # in the same way as all the other instances using a class.
ActionCable = Class.new(RedisBase)
- Cache = Class.new(RedisBase).enable_redis_cluster_validation
- Queues = Class.new(RedisBase)
- SharedState = Class.new(RedisBase).enable_redis_cluster_validation
- TraceChunks = Class.new(RedisBase).enable_redis_cluster_validation
- RateLimiting = Class.new(RedisBase).enable_redis_cluster_validation
- Sessions = Class.new(RedisBase).enable_redis_cluster_validation
-
- STORAGES = [ActionCable, Cache, Queues, SharedState, TraceChunks, RateLimiting, Sessions].freeze
+
+ STORAGES = (
+ Gitlab::Redis::ALL_CLASSES.map do |redis_instance_class|
+ instrumentation_class = Class.new(RedisBase)
+
+ instrumentation_class.enable_redis_cluster_validation unless redis_instance_class == Gitlab::Redis::Queues
+
+ const_set(redis_instance_class.store_name, instrumentation_class)
+ instrumentation_class
+ end << ActionCable
+ ).freeze
# Milliseconds represented in seconds (from 1 millisecond to 2 seconds).
QUERY_TIME_BUCKETS = [0.001, 0.0025, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2].freeze
diff --git a/lib/gitlab/instrumentation/redis_base.rb b/lib/gitlab/instrumentation/redis_base.rb
index 0beab008f73..0bd10597f24 100644
--- a/lib/gitlab/instrumentation/redis_base.rb
+++ b/lib/gitlab/instrumentation/redis_base.rb
@@ -20,21 +20,19 @@ module Gitlab
::RequestStore[call_duration_key] += duration
end
- def add_call_details(duration, args)
+ def add_call_details(duration, commands)
return unless Gitlab::PerformanceBar.enabled_for_request?
- # redis-rb passes an array (e.g. [[:get, key]])
- return unless args.length == 1
detail_store << {
- cmd: args.first,
+ commands: commands,
duration: duration,
backtrace: ::Gitlab::BacktraceCleaner.clean_backtrace(caller)
}
end
- def increment_request_count
+ def increment_request_count(amount = 1)
::RequestStore[request_count_key] ||= 0
- ::RequestStore[request_count_key] += 1
+ ::RequestStore[request_count_key] += amount
end
def increment_read_bytes(num_bytes)
@@ -78,9 +76,9 @@ module Gitlab
self
end
- def instance_count_request
+ def instance_count_request(amount = 1)
@request_counter ||= Gitlab::Metrics.counter(:gitlab_redis_client_requests_total, 'Client side Redis request count, per Redis server')
- @request_counter.increment({ storage: storage_key })
+ @request_counter.increment({ storage: storage_key }, amount)
end
def instance_count_exception(ex)
diff --git a/lib/gitlab/instrumentation/redis_interceptor.rb b/lib/gitlab/instrumentation/redis_interceptor.rb
index 14474693ddf..7e2acb91b94 100644
--- a/lib/gitlab/instrumentation/redis_interceptor.rb
+++ b/lib/gitlab/instrumentation/redis_interceptor.rb
@@ -13,27 +13,15 @@ module Gitlab
end
end
- def call(*args, &block)
- start = Gitlab::Metrics::System.monotonic_time # must come first so that 'start' is always defined
- instrumentation_class.instance_count_request
- instrumentation_class.redis_cluster_validate!(args.first)
-
- super(*args, &block)
- rescue ::Redis::BaseError => ex
- instrumentation_class.instance_count_exception(ex)
- raise ex
- ensure
- duration = Gitlab::Metrics::System.monotonic_time - start
-
- unless APDEX_EXCLUDE.include?(command_from_args(args))
- instrumentation_class.instance_observe_duration(duration)
+ def call(command)
+ instrument_call([command]) do
+ super
end
+ end
- if ::RequestStore.active?
- # These metrics measure total Redis usage per Rails request / job.
- instrumentation_class.increment_request_count
- instrumentation_class.add_duration(duration)
- instrumentation_class.add_call_details(duration, args)
+ def call_pipeline(pipeline)
+ instrument_call(pipeline.commands) do
+ super
end
end
@@ -50,6 +38,31 @@ module Gitlab
private
+ def instrument_call(commands)
+ start = Gitlab::Metrics::System.monotonic_time # must come first so that 'start' is always defined
+ instrumentation_class.instance_count_request(commands.size)
+
+ commands.each { |c| instrumentation_class.redis_cluster_validate!(c) }
+
+ yield
+ rescue ::Redis::BaseError => ex
+ instrumentation_class.instance_count_exception(ex)
+ raise ex
+ ensure
+ duration = Gitlab::Metrics::System.monotonic_time - start
+
+ unless exclude_from_apdex?(commands)
+ commands.each { instrumentation_class.instance_observe_duration(duration / commands.size) }
+ end
+
+ if ::RequestStore.active?
+ # These metrics measure total Redis usage per Rails request / job.
+ instrumentation_class.increment_request_count(commands.size)
+ instrumentation_class.add_duration(duration)
+ instrumentation_class.add_call_details(duration, commands)
+ end
+ end
+
def measure_write_size(command)
size = 0
@@ -97,10 +110,8 @@ module Gitlab
@options[:instrumentation_class] # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
- def command_from_args(args)
- command = args[0]
- command = command[0] if command.is_a?(Array)
- command.to_s.downcase
+ def exclude_from_apdex?(commands)
+ commands.any? { |command| APDEX_EXCLUDE.include?(command.first.to_s.downcase) }
end
end
end
diff --git a/lib/gitlab/issuable/clone/copy_resource_events_service.rb b/lib/gitlab/issuable/clone/copy_resource_events_service.rb
index 563805fcb01..448ac4c2ae0 100644
--- a/lib/gitlab/issuable/clone/copy_resource_events_service.rb
+++ b/lib/gitlab/issuable/clone/copy_resource_events_service.rb
@@ -49,7 +49,7 @@ module Gitlab
event.attributes
.except(*blocked_state_event_attributes)
.merge(entity_key => new_entity.id,
- 'state' => ResourceStateEvent.states[event.state])
+ 'state' => ResourceStateEvent.states[event.state])
end
end
@@ -62,9 +62,9 @@ module Gitlab
event.attributes
.except('id')
.merge(entity_key => new_entity.id,
- 'milestone_id' => milestone&.id,
- 'action' => ResourceMilestoneEvent.actions[event.action],
- 'state' => ResourceMilestoneEvent.states[event.state])
+ 'milestone_id' => milestone&.id,
+ 'action' => ResourceMilestoneEvent.actions[event.action],
+ 'state' => ResourceMilestoneEvent.states[event.state])
end
def copy_events(table_name, events_to_copy)
diff --git a/lib/gitlab/jira_import.rb b/lib/gitlab/jira_import.rb
index 60344e4be68..fd41d9eeb5a 100644
--- a/lib/gitlab/jira_import.rb
+++ b/lib/gitlab/jira_import.rb
@@ -66,11 +66,6 @@ module Gitlab
cache_class.write(cache_key, value)
end
- def self.cache_issue_mapping(issue_id, jira_issue_id, project_id)
- cache_key = JiraImport.jira_item_cache_key(project_id, jira_issue_id, :issues)
- cache_class.write(cache_key, issue_id)
- end
-
def self.get_import_label_id(project_id)
cache_class.read(JiraImport.import_label_cache_key(project_id))
end
diff --git a/lib/gitlab/kubernetes.rb b/lib/gitlab/kubernetes.rb
index 15163bd4a57..0a0a1defd11 100644
--- a/lib/gitlab/kubernetes.rb
+++ b/lib/gitlab/kubernetes.rb
@@ -71,11 +71,11 @@ module Gitlab
containers.map do |container|
{
- selectors: { pod: pod_name, container: container["name"] },
- url: container_exec_url(api_url, namespace, pod_name, container["name"]),
+ selectors: { pod: pod_name, container: container["name"] },
+ url: container_exec_url(api_url, namespace, pod_name, container["name"]),
subprotocols: ['channel.k8s.io'],
- headers: ::Gitlab::Kubernetes.build_header_hash,
- created_at: created_at
+ headers: ::Gitlab::Kubernetes.build_header_hash,
+ created_at: created_at
}
end
end
diff --git a/lib/gitlab/legacy_github_import/client.rb b/lib/gitlab/legacy_github_import/client.rb
index 7d78c8dee25..dd1502bbbcd 100644
--- a/lib/gitlab/legacy_github_import/client.rb
+++ b/lib/gitlab/legacy_github_import/client.rb
@@ -79,6 +79,20 @@ module Gitlab
@users[login] = api.user(login)
end
+ def repository(id)
+ request(:repository, id).to_h
+ end
+
+ def repos
+ repositories = request(:repos, nil)
+
+ if repositories.is_a?(Array)
+ repositories.map(&:to_h)
+ else
+ repositories
+ end
+ end
+
private
def api_endpoint
diff --git a/lib/gitlab/legacy_github_import/project_creator.rb b/lib/gitlab/legacy_github_import/project_creator.rb
index c54325bcdf5..01e04fa9c81 100644
--- a/lib/gitlab/legacy_github_import/project_creator.rb
+++ b/lib/gitlab/legacy_github_import/project_creator.rb
@@ -18,11 +18,11 @@ module Gitlab
attrs = {
name: name,
path: name,
- description: repo.description,
+ description: repo[:description],
namespace_id: namespace.id,
visibility_level: visibility_level,
import_type: type,
- import_source: repo.full_name,
+ import_source: repo[:full_name],
import_url: import_url,
skip_wiki: skip_wiki
}.merge!(extra_attrs)
@@ -33,11 +33,11 @@ module Gitlab
private
def import_url
- repo.clone_url.sub('://', "://#{session_data[:github_access_token]}@")
+ repo[:clone_url].sub('://', "://#{session_data[:github_access_token]}@")
end
def visibility_level
- visibility_level = repo.private ? Gitlab::VisibilityLevel::PRIVATE : @namespace.visibility_level
+ visibility_level = repo[:private] ? Gitlab::VisibilityLevel::PRIVATE : @namespace.visibility_level
visibility_level = Gitlab::CurrentSettings.default_project_visibility if Gitlab::CurrentSettings.restricted_visibility_levels.include?(visibility_level)
visibility_level
@@ -49,7 +49,7 @@ module Gitlab
# repository already exist.
#
def skip_wiki
- repo.has_wiki?
+ repo[:has_wiki]
end
end
end
diff --git a/lib/gitlab/mailgun/webhook_processors/failure_logger.rb b/lib/gitlab/mailgun/webhook_processors/failure_logger.rb
index a7a85bd1672..fa72abf1311 100644
--- a/lib/gitlab/mailgun/webhook_processors/failure_logger.rb
+++ b/lib/gitlab/mailgun/webhook_processors/failure_logger.rb
@@ -5,11 +5,12 @@ module Gitlab
module WebhookProcessors
class FailureLogger < Base
def execute
- log_failure if permanent_failure? || temporary_failure_over_threshold?
+ log_failure if permanent_failure_over_threshold? || temporary_failure_over_threshold?
end
- def permanent_failure?
- payload['event'] == 'failed' && payload['severity'] == 'permanent'
+ def permanent_failure_over_threshold?
+ payload['event'] == 'failed' && payload['severity'] == 'permanent' &&
+ Gitlab::ApplicationRateLimiter.throttled?(:permanent_email_failure, scope: payload['recipient'])
end
def temporary_failure_over_threshold?
diff --git a/lib/gitlab/manifest_import/metadata.rb b/lib/gitlab/manifest_import/metadata.rb
index 80dff075391..6fe9bb10cdf 100644
--- a/lib/gitlab/manifest_import/metadata.rb
+++ b/lib/gitlab/manifest_import/metadata.rb
@@ -14,9 +14,9 @@ module Gitlab
def save(repositories, group_id)
Gitlab::Redis::SharedState.with do |redis|
- redis.multi do
- redis.set(key_for('repositories'), Gitlab::Json.dump(repositories), ex: EXPIRY_TIME)
- redis.set(key_for('group_id'), group_id, ex: EXPIRY_TIME)
+ redis.multi do |multi|
+ multi.set(key_for('repositories'), Gitlab::Json.dump(repositories), ex: EXPIRY_TIME)
+ multi.set(key_for('group_id'), group_id, ex: EXPIRY_TIME)
end
end
end
diff --git a/lib/gitlab/marginalia/comment.rb b/lib/gitlab/marginalia/comment.rb
index f635f41ec39..aab58bfa211 100644
--- a/lib/gitlab/marginalia/comment.rb
+++ b/lib/gitlab/marginalia/comment.rb
@@ -31,7 +31,7 @@ module Gitlab
if job.is_a?(ActionMailer::MailDeliveryJob)
{
"class" => job.arguments.first,
- "jid" => job.job_id
+ "jid" => job.job_id
}
else
job
diff --git a/lib/gitlab/markdown_cache/redis/store.rb b/lib/gitlab/markdown_cache/redis/store.rb
index 5a8efa34097..752ab153f37 100644
--- a/lib/gitlab/markdown_cache/redis/store.rb
+++ b/lib/gitlab/markdown_cache/redis/store.rb
@@ -10,9 +10,9 @@ module Gitlab
results = {}
Gitlab::Redis::Cache.with do |r|
- r.pipelined do
+ r.pipelined do |pipeline|
subjects.each do |subject|
- results[subject.cache_key] = new(subject).read
+ results[subject.cache_key] = new(subject).read(pipeline)
end
end
end
@@ -34,11 +34,15 @@ module Gitlab
end
end
- def read
+ def read(pipeline = nil)
@loaded = true
- Gitlab::Redis::Cache.with do |r|
- r.mapped_hmget(markdown_cache_key, *fields)
+ if pipeline
+ pipeline.mapped_hmget(markdown_cache_key, *fields)
+ else
+ Gitlab::Redis::Cache.with do |r|
+ r.mapped_hmget(markdown_cache_key, *fields)
+ end
end
end
diff --git a/lib/gitlab/memory/jemalloc.rb b/lib/gitlab/memory/jemalloc.rb
index 7163a70a5cb..e20e186cab9 100644
--- a/lib/gitlab/memory/jemalloc.rb
+++ b/lib/gitlab/memory/jemalloc.rb
@@ -27,21 +27,27 @@ module Gitlab
# Write jemalloc stats to the given directory
# @param [String] path Directory path the dump will be put into
+ # @param [String] tmp_dir Directory path the dump will be streaming to. It is moved to `path` when finished.
# @param [String] format `json` or `txt`
# @param [String] filename_label Optional custom string that will be injected into the file name, e.g. `worker_0`
# @return [String] Full path to the resulting dump file
- def dump_stats(path:, format: STATS_DEFAULT_FORMAT, filename_label: nil)
+ def dump_stats(path:, tmp_dir: Dir.tmpdir, format: STATS_DEFAULT_FORMAT, filename_label: nil)
verify_format!(format)
format_settings = STATS_FORMATS[format]
+ tmp_file_path = File.join(tmp_dir, file_name(format_settings[:extension], filename_label))
file_path = File.join(path, file_name(format_settings[:extension], filename_label))
with_malloc_stats_print do |stats_print|
- File.open(file_path, 'wb') do |io|
+ File.open(tmp_file_path, 'wb') do |io|
write_stats(stats_print, io, format_settings)
end
end
+ # On OSX, `with_malloc_stats_print` is no-op, and, as result, no file will be written
+ return unless File.exist?(tmp_file_path)
+
+ FileUtils.mv(tmp_file_path, file_path)
file_path
end
diff --git a/lib/gitlab/memory/reports/jemalloc_stats.rb b/lib/gitlab/memory/reports/jemalloc_stats.rb
index b99bec4ac3e..05f0717d7c3 100644
--- a/lib/gitlab/memory/reports/jemalloc_stats.rb
+++ b/lib/gitlab/memory/reports/jemalloc_stats.rb
@@ -18,12 +18,19 @@ module Gitlab
def initialize(reports_path:)
@reports_path = reports_path
+
+ # Store report in tmp subdir while it is still streaming.
+ # This will clearly separate finished reports from the files we are still writing to.
+ @tmp_dir = File.join(@reports_path, 'tmp')
+ FileUtils.mkdir_p(@tmp_dir)
end
def run
return unless active?
- Gitlab::Memory::Jemalloc.dump_stats(path: reports_path, filename_label: worker_id).tap { cleanup }
+ Gitlab::Memory::Jemalloc.dump_stats(path: reports_path, tmp_dir: @tmp_dir, filename_label: worker_id).tap do
+ cleanup
+ end
end
def active?
diff --git a/lib/gitlab/memory/watchdog.rb b/lib/gitlab/memory/watchdog.rb
index 91edb68ad66..38231fa933b 100644
--- a/lib/gitlab/memory/watchdog.rb
+++ b/lib/gitlab/memory/watchdog.rb
@@ -16,8 +16,9 @@ module Gitlab
# The duration for which a process may be above a given fragmentation
# threshold is computed as `max_strikes * sleep_time_seconds`.
class Watchdog
- DEFAULT_SLEEP_TIME_SECONDS = 60
- DEFAULT_HEAP_FRAG_THRESHOLD = 0.5
+ DEFAULT_SLEEP_TIME_SECONDS = 60 * 5
+ DEFAULT_MAX_HEAP_FRAG = 0.5
+ DEFAULT_MAX_MEM_GROWTH = 3.0
DEFAULT_MAX_STRIKES = 5
# This handler does nothing. It returns `false` to indicate to the
@@ -29,7 +30,7 @@ module Gitlab
class NullHandler
include Singleton
- def on_high_heap_fragmentation(value)
+ def call
# NOP
false
end
@@ -41,7 +42,7 @@ module Gitlab
@pid = pid
end
- def on_high_heap_fragmentation(value)
+ def call
Process.kill(:TERM, @pid)
true
end
@@ -55,7 +56,7 @@ module Gitlab
@worker = ::Puma::Cluster::WorkerHandle.new(0, $$, 0, puma_options)
end
- def on_high_heap_fragmentation(value)
+ def call
@worker.term
true
end
@@ -63,6 +64,9 @@ module Gitlab
# max_heap_fragmentation:
# The degree to which the Ruby heap is allowed to be fragmented. Range [0,1].
+ # max_mem_growth:
+ # A multiplier for how much excess private memory a worker can map compared to a reference process
+ # (itself or the primary in a pre-fork server.)
# max_strikes:
# How many times the process is allowed to be above max_heap_fragmentation before
# a handler is invoked.
@@ -71,7 +75,8 @@ module Gitlab
def initialize(
handler: NullHandler.instance,
logger: Logger.new($stdout),
- max_heap_fragmentation: ENV['GITLAB_MEMWD_MAX_HEAP_FRAG']&.to_f || DEFAULT_HEAP_FRAG_THRESHOLD,
+ max_heap_fragmentation: ENV['GITLAB_MEMWD_MAX_HEAP_FRAG']&.to_f || DEFAULT_MAX_HEAP_FRAG,
+ max_mem_growth: ENV['GITLAB_MEMWD_MAX_MEM_GROWTH']&.to_f || DEFAULT_MAX_MEM_GROWTH,
max_strikes: ENV['GITLAB_MEMWD_MAX_STRIKES']&.to_i || DEFAULT_MAX_STRIKES,
sleep_time_seconds: ENV['GITLAB_MEMWD_SLEEP_TIME_SEC']&.to_i || DEFAULT_SLEEP_TIME_SECONDS,
**options)
@@ -79,17 +84,37 @@ module Gitlab
@handler = handler
@logger = logger
- @max_heap_fragmentation = max_heap_fragmentation
@sleep_time_seconds = sleep_time_seconds
@max_strikes = max_strikes
+ @stats = {
+ heap_frag: {
+ max: max_heap_fragmentation,
+ strikes: 0
+ },
+ mem_growth: {
+ max: max_mem_growth,
+ strikes: 0
+ }
+ }
@alive = true
- @strikes = 0
init_prometheus_metrics(max_heap_fragmentation)
end
- attr_reader :strikes, :max_heap_fragmentation, :max_strikes, :sleep_time_seconds
+ attr_reader :max_strikes, :sleep_time_seconds
+
+ def max_heap_fragmentation
+ @stats[:heap_frag][:max]
+ end
+
+ def max_mem_growth
+ @stats[:mem_growth][:max]
+ end
+
+ def strikes(stat)
+ @stats[stat][:strikes]
+ end
def call
@logger.info(log_labels.merge(message: 'started'))
@@ -97,7 +122,10 @@ module Gitlab
while @alive
sleep(@sleep_time_seconds)
- monitor_heap_fragmentation if Feature.enabled?(:gitlab_memory_watchdog, type: :ops)
+ next unless Feature.enabled?(:gitlab_memory_watchdog, type: :ops)
+
+ monitor_heap_fragmentation
+ monitor_memory_growth
end
@logger.info(log_labels.merge(message: 'stopped'))
@@ -109,32 +137,73 @@ module Gitlab
private
- def monitor_heap_fragmentation
- heap_fragmentation = Gitlab::Metrics::Memory.gc_heap_fragmentation
+ def monitor_memory_condition(stat_key)
+ return unless @alive
+
+ stat = @stats[stat_key]
+
+ ok, labels = yield(stat)
- if heap_fragmentation > @max_heap_fragmentation
- @strikes += 1
- @heap_frag_violations.increment
+ if ok
+ stat[:strikes] = 0
else
- @strikes = 0
+ stat[:strikes] += 1
+ @counter_violations.increment(reason: stat_key.to_s)
end
- if @strikes > @max_strikes
- # If the handler returns true, it means the event is handled and we can shut down.
- @alive = !handle_heap_fragmentation_limit_exceeded(heap_fragmentation)
- @strikes = 0
+ if stat[:strikes] > @max_strikes
+ @alive = !memory_limit_exceeded_callback(stat_key, labels)
+ stat[:strikes] = 0
end
end
- def handle_heap_fragmentation_limit_exceeded(value)
- @logger.warn(
- log_labels.merge(
- message: 'heap fragmentation limit exceeded',
- memwd_cur_heap_frag: value
- ))
- @heap_frag_violations_handled.increment
+ def monitor_heap_fragmentation
+ monitor_memory_condition(:heap_frag) do |stat|
+ heap_fragmentation = Gitlab::Metrics::Memory.gc_heap_fragmentation
+ [
+ heap_fragmentation <= stat[:max],
+ {
+ message: 'heap fragmentation limit exceeded',
+ memwd_cur_heap_frag: heap_fragmentation,
+ memwd_max_heap_frag: stat[:max]
+ }
+ ]
+ end
+ end
+
+ def monitor_memory_growth
+ monitor_memory_condition(:mem_growth) do |stat|
+ worker_uss = Gitlab::Metrics::System.memory_usage_uss_pss[:uss]
+ reference_uss = reference_mem[:uss]
+ memory_limit = stat[:max] * reference_uss
+ [
+ worker_uss <= memory_limit,
+ {
+ message: 'memory limit exceeded',
+ memwd_uss_bytes: worker_uss,
+ memwd_ref_uss_bytes: reference_uss,
+ memwd_max_uss_bytes: memory_limit
+ }
+ ]
+ end
+ end
+
+ # On pre-fork systems this would be the primary process memory from which workers fork.
+ # Otherwise it is the current process' memory.
+ #
+ # We initialize this lazily because in the initializer the application may not have
+ # finished booting yet, which would yield an incorrect baseline.
+ def reference_mem
+ @reference_mem ||= Gitlab::Metrics::System.memory_usage_uss_pss(pid: Gitlab::Cluster::PRIMARY_PID)
+ end
+
+ def memory_limit_exceeded_callback(stat_key, handler_labels)
+ all_labels = log_labels.merge(handler_labels)
+ .merge(memwd_cur_strikes: strikes(stat_key))
+ @logger.warn(all_labels)
+ @counter_violations_handled.increment(reason: stat_key.to_s)
- handler.on_high_heap_fragmentation(value)
+ handler.call
end
def handler
@@ -151,9 +220,7 @@ module Gitlab
worker_id: worker_id,
memwd_handler_class: handler.class.name,
memwd_sleep_time_s: @sleep_time_seconds,
- memwd_max_heap_frag: @max_heap_fragmentation,
memwd_max_strikes: @max_strikes,
- memwd_cur_strikes: @strikes,
memwd_rss_bytes: process_rss_bytes
}
end
@@ -174,14 +241,14 @@ module Gitlab
@heap_frag_limit.set({}, max_heap_fragmentation)
default_labels = { pid: worker_id }
- @heap_frag_violations = Gitlab::Metrics.counter(
- :gitlab_memwd_heap_frag_violations_total,
- 'Total number of times heap fragmentation in a Ruby process exceeded its allowed maximum',
+ @counter_violations = Gitlab::Metrics.counter(
+ :gitlab_memwd_violations_total,
+ 'Total number of times a Ruby process violated a memory threshold',
default_labels
)
- @heap_frag_violations_handled = Gitlab::Metrics.counter(
- :gitlab_memwd_heap_frag_violations_handled_total,
- 'Total number of times heap fragmentation violations in a Ruby process were handled',
+ @counter_violations_handled = Gitlab::Metrics.counter(
+ :gitlab_memwd_violations_handled_total,
+ 'Total number of times Ruby process memory violations were handled',
default_labels
)
end
diff --git a/lib/gitlab/metrics/dashboard/stages/grafana_formatter.rb b/lib/gitlab/metrics/dashboard/stages/grafana_formatter.rb
index 55d14d6f94a..622b6adec7e 100644
--- a/lib/gitlab/metrics/dashboard/stages/grafana_formatter.rb
+++ b/lib/gitlab/metrics/dashboard/stages/grafana_formatter.rb
@@ -40,8 +40,8 @@ module Gitlab
def formatted_panel
{
- title: panel[:title],
- type: CHART_TYPE,
+ title: panel[:title],
+ type: CHART_TYPE,
y_label: '', # Grafana panels do not include a Y-Axis label
metrics: panel[:targets].map.with_index do |target, idx|
formatted_metric(target, idx)
@@ -51,9 +51,9 @@ module Gitlab
def formatted_metric(metric, idx)
{
- id: "#{metric[:legendFormat]}_#{idx}",
- query_range: format_query(metric),
- label: replace_variables(metric[:legendFormat]),
+ id: "#{metric[:legendFormat]}_#{idx}",
+ query_range: format_query(metric),
+ label: replace_variables(metric[:legendFormat]),
prometheus_endpoint_path: prometheus_endpoint_for_metric(metric)
}.compact
end
diff --git a/lib/gitlab/metrics/dashboard/transformers/yml/v1/prometheus_metrics.rb b/lib/gitlab/metrics/dashboard/transformers/yml/v1/prometheus_metrics.rb
index 4e46eec17d6..3650ddf698a 100644
--- a/lib/gitlab/metrics/dashboard/transformers/yml/v1/prometheus_metrics.rb
+++ b/lib/gitlab/metrics/dashboard/transformers/yml/v1/prometheus_metrics.rb
@@ -24,15 +24,15 @@ module Gitlab
panel_group[:panels].each do |panel|
panel[:metrics].each do |metric|
prometheus_metrics << {
- project: project,
- title: panel[:title],
- y_label: panel[:y_label],
- query: metric[:query_range] || metric[:query],
- unit: metric[:unit],
- legend: metric[:label],
- identifier: metric[:id],
- group: Enums::PrometheusMetric.groups[:custom],
- common: false,
+ project: project,
+ title: panel[:title],
+ y_label: panel[:y_label],
+ query: metric[:query_range] || metric[:query],
+ unit: metric[:unit],
+ legend: metric[:label],
+ identifier: metric[:id],
+ group: Enums::PrometheusMetric.groups[:custom],
+ common: false,
dashboard_path: dashboard_path
}.compact
end
diff --git a/lib/gitlab/metrics/dashboard/validator/client.rb b/lib/gitlab/metrics/dashboard/validator/client.rb
index 588c677ca28..29f1274a097 100644
--- a/lib/gitlab/metrics/dashboard/validator/client.rb
+++ b/lib/gitlab/metrics/dashboard/validator/client.rb
@@ -34,8 +34,8 @@ module Gitlab
def post_schema_validator
PostSchemaValidator.new(
- project: project,
- metric_ids: custom_formats.metric_ids_cache,
+ project: project,
+ metric_ids: custom_formats.metric_ids_cache,
dashboard_path: dashboard_path
)
end
diff --git a/lib/gitlab/metrics/exporter/metrics_middleware.rb b/lib/gitlab/metrics/exporter/metrics_middleware.rb
index e17f1c13cf0..258b655229e 100644
--- a/lib/gitlab/metrics/exporter/metrics_middleware.rb
+++ b/lib/gitlab/metrics/exporter/metrics_middleware.rb
@@ -27,8 +27,8 @@ module Gitlab
labels = {
method: env['REQUEST_METHOD'].downcase,
- path: env['PATH_INFO'].to_s,
- code: response.first.to_s
+ path: env['PATH_INFO'].to_s,
+ code: response.first.to_s
}
@requests_total.increment(labels)
diff --git a/lib/gitlab/metrics/global_search_slis.rb b/lib/gitlab/metrics/global_search_slis.rb
new file mode 100644
index 00000000000..e37129fed38
--- /dev/null
+++ b/lib/gitlab/metrics/global_search_slis.rb
@@ -0,0 +1,114 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Metrics
+ module GlobalSearchSlis
+ class << self
+ # The following targets are the 99.95th percentile of code searches
+ # gathered on 24-08-2022
+ # from https://log.gprd.gitlab.net/goto/0c89cd80-23af-11ed-8656-f5f2137823ba (internal only)
+ BASIC_CONTENT_TARGET_S = 7.031
+ BASIC_CODE_TARGET_S = 21.903
+ ADVANCED_CONTENT_TARGET_S = 4.865
+ ADVANCED_CODE_TARGET_S = 13.546
+
+ def initialize_slis!
+ if Feature.enabled?(:global_search_custom_slis)
+ Gitlab::Metrics::Sli::Apdex.initialize_sli(:global_search, possible_labels)
+ end
+
+ return unless Feature.enabled?(:global_search_error_rate_sli)
+
+ Gitlab::Metrics::Sli::ErrorRate.initialize_sli(:global_search, possible_labels)
+ end
+
+ def record_apdex(elapsed:, search_type:, search_level:, search_scope:)
+ return unless Feature.enabled?(:global_search_custom_slis)
+
+ Gitlab::Metrics::Sli::Apdex[:global_search].increment(
+ labels: labels(search_type: search_type, search_level: search_level, search_scope: search_scope),
+ success: elapsed < duration_target(search_type, search_scope)
+ )
+ end
+
+ def record_error_rate(error:, search_type:, search_level:, search_scope:)
+ return unless Feature.enabled?(:global_search_error_rate_sli)
+
+ Gitlab::Metrics::Sli::ErrorRate[:global_search].increment(
+ labels: labels(search_type: search_type, search_level: search_level, search_scope: search_scope),
+ error: error
+ )
+ end
+
+ private
+
+ def duration_target(search_type, search_scope)
+ if search_type == 'basic' && content_search?(search_scope)
+ BASIC_CONTENT_TARGET_S
+ elsif search_type == 'basic' && code_search?(search_scope)
+ BASIC_CODE_TARGET_S
+ elsif search_type == 'advanced' && content_search?(search_scope)
+ ADVANCED_CONTENT_TARGET_S
+ elsif search_type == 'advanced' && code_search?(search_scope)
+ ADVANCED_CODE_TARGET_S
+ end
+ end
+
+ def search_types
+ %w[basic advanced]
+ end
+
+ def search_levels
+ %w[project group global]
+ end
+
+ def search_scopes
+ Gitlab::Search::AbuseDetection::ALLOWED_SCOPES
+ end
+
+ def endpoint_ids
+ ['SearchController#show', 'GET /api/:version/search', 'GET /api/:version/projects/:id/(-/)search',
+ 'GET /api/:version/groups/:id/(-/)search']
+ end
+
+ def possible_labels
+ search_types.flat_map do |search_type|
+ search_levels.flat_map do |search_level|
+ search_scopes.flat_map do |search_scope|
+ endpoint_ids.flat_map do |endpoint_id|
+ {
+ search_type: search_type,
+ search_level: search_level,
+ search_scope: search_scope,
+ endpoint_id: endpoint_id
+ }
+ end
+ end
+ end
+ end
+ end
+
+ def labels(search_type:, search_level:, search_scope:)
+ {
+ search_type: search_type,
+ search_level: search_level,
+ search_scope: search_scope,
+ endpoint_id: endpoint_id
+ }
+ end
+
+ def endpoint_id
+ ::Gitlab::ApplicationContext.current_context_attribute(:caller_id)
+ end
+
+ def code_search?(search_scope)
+ search_scope == 'blobs'
+ end
+
+ def content_search?(search_scope)
+ !code_search?(search_scope)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/metrics/samplers/puma_sampler.rb b/lib/gitlab/metrics/samplers/puma_sampler.rb
index 848a55e59ff..d818aa43853 100644
--- a/lib/gitlab/metrics/samplers/puma_sampler.rb
+++ b/lib/gitlab/metrics/samplers/puma_sampler.rb
@@ -12,15 +12,15 @@ module Gitlab
def init_metrics
{
- puma_workers: ::Gitlab::Metrics.gauge(:puma_workers, 'Total number of workers'),
- puma_running_workers: ::Gitlab::Metrics.gauge(:puma_running_workers, 'Number of active workers'),
- puma_stale_workers: ::Gitlab::Metrics.gauge(:puma_stale_workers, 'Number of stale workers'),
- puma_running: ::Gitlab::Metrics.gauge(:puma_running, 'Number of running threads'),
+ puma_workers: ::Gitlab::Metrics.gauge(:puma_workers, 'Total number of workers'),
+ puma_running_workers: ::Gitlab::Metrics.gauge(:puma_running_workers, 'Number of active workers'),
+ puma_stale_workers: ::Gitlab::Metrics.gauge(:puma_stale_workers, 'Number of stale workers'),
+ puma_running: ::Gitlab::Metrics.gauge(:puma_running, 'Number of running threads'),
puma_queued_connections: ::Gitlab::Metrics.gauge(:puma_queued_connections, 'Number of connections in that worker\'s "todo" set waiting for a worker thread'),
puma_active_connections: ::Gitlab::Metrics.gauge(:puma_active_connections, 'Number of threads processing a request'),
- puma_pool_capacity: ::Gitlab::Metrics.gauge(:puma_pool_capacity, 'Number of requests the worker is capable of taking right now'),
- puma_max_threads: ::Gitlab::Metrics.gauge(:puma_max_threads, 'Maximum number of worker threads'),
- puma_idle_threads: ::Gitlab::Metrics.gauge(:puma_idle_threads, 'Number of spawned threads which are not processing a request')
+ puma_pool_capacity: ::Gitlab::Metrics.gauge(:puma_pool_capacity, 'Number of requests the worker is capable of taking right now'),
+ puma_max_threads: ::Gitlab::Metrics.gauge(:puma_max_threads, 'Maximum number of worker threads'),
+ puma_idle_threads: ::Gitlab::Metrics.gauge(:puma_idle_threads, 'Number of spawned threads which are not processing a request')
}
end
diff --git a/lib/gitlab/metrics/samplers/ruby_sampler.rb b/lib/gitlab/metrics/samplers/ruby_sampler.rb
index 8e002293347..4fe338ffc7f 100644
--- a/lib/gitlab/metrics/samplers/ruby_sampler.rb
+++ b/lib/gitlab/metrics/samplers/ruby_sampler.rb
@@ -31,16 +31,16 @@ module Gitlab
def init_metrics
metrics = {
- file_descriptors: ::Gitlab::Metrics.gauge(metric_name(:file, :descriptors), 'File descriptors used', labels),
- process_cpu_seconds_total: ::Gitlab::Metrics.gauge(metric_name(:process, :cpu_seconds_total), 'Process CPU seconds total'),
- process_max_fds: ::Gitlab::Metrics.gauge(metric_name(:process, :max_fds), 'Process max fds'),
- process_resident_memory_bytes: ::Gitlab::Metrics.gauge(metric_name(:process, :resident_memory_bytes), 'Memory used (RSS)', labels),
- process_unique_memory_bytes: ::Gitlab::Metrics.gauge(metric_name(:process, :unique_memory_bytes), 'Memory used (USS)', labels),
+ file_descriptors: ::Gitlab::Metrics.gauge(metric_name(:file, :descriptors), 'File descriptors used', labels),
+ process_cpu_seconds_total: ::Gitlab::Metrics.gauge(metric_name(:process, :cpu_seconds_total), 'Process CPU seconds total'),
+ process_max_fds: ::Gitlab::Metrics.gauge(metric_name(:process, :max_fds), 'Process max fds'),
+ process_resident_memory_bytes: ::Gitlab::Metrics.gauge(metric_name(:process, :resident_memory_bytes), 'Memory used (RSS)', labels),
+ process_unique_memory_bytes: ::Gitlab::Metrics.gauge(metric_name(:process, :unique_memory_bytes), 'Memory used (USS)', labels),
process_proportional_memory_bytes: ::Gitlab::Metrics.gauge(metric_name(:process, :proportional_memory_bytes), 'Memory used (PSS)', labels),
- process_start_time_seconds: ::Gitlab::Metrics.gauge(metric_name(:process, :start_time_seconds), 'Process start time seconds'),
- sampler_duration: ::Gitlab::Metrics.counter(metric_name(:sampler, :duration_seconds_total), 'Sampler time', labels),
- gc_duration_seconds: ::Gitlab::Metrics.histogram(metric_name(:gc, :duration_seconds), 'GC time', labels, GC_REPORT_BUCKETS),
- heap_fragmentation: ::Gitlab::Metrics.gauge(metric_name(:gc_stat_ext, :heap_fragmentation), 'Ruby heap fragmentation', labels)
+ process_start_time_seconds: ::Gitlab::Metrics.gauge(metric_name(:process, :start_time_seconds), 'Process start time seconds'),
+ sampler_duration: ::Gitlab::Metrics.counter(metric_name(:sampler, :duration_seconds_total), 'Sampler time', labels),
+ gc_duration_seconds: ::Gitlab::Metrics.histogram(metric_name(:gc, :duration_seconds), 'GC time', labels, GC_REPORT_BUCKETS),
+ heap_fragmentation: ::Gitlab::Metrics.gauge(metric_name(:gc_stat_ext, :heap_fragmentation), 'Ruby heap fragmentation', labels)
}
GC.stat.keys.each do |key|
diff --git a/lib/gitlab/metrics/system.rb b/lib/gitlab/metrics/system.rb
index e646846face..d7eef722d6e 100644
--- a/lib/gitlab/metrics/system.rb
+++ b/lib/gitlab/metrics/system.rb
@@ -10,8 +10,8 @@ module Gitlab
extend self
PROC_STAT_PATH = '/proc/self/stat'
- PROC_STATUS_PATH = '/proc/self/status'
- PROC_SMAPS_ROLLUP_PATH = '/proc/self/smaps_rollup'
+ PROC_STATUS_PATH = '/proc/%s/status'
+ PROC_SMAPS_ROLLUP_PATH = '/proc/%s/smaps_rollup'
PROC_LIMITS_PATH = '/proc/self/limits'
PROC_FD_GLOB = '/proc/self/fd/*'
@@ -34,14 +34,14 @@ module Gitlab
}
end
- # Returns the current process' RSS (resident set size) in bytes.
- def memory_usage_rss
- sum_matches(PROC_STATUS_PATH, rss: RSS_PATTERN)[:rss].kilobytes
+ # Returns the given process' RSS (resident set size) in bytes.
+ def memory_usage_rss(pid: 'self')
+ sum_matches(PROC_STATUS_PATH % pid, rss: RSS_PATTERN)[:rss].kilobytes
end
- # Returns the current process' USS/PSS (unique/proportional set size) in bytes.
- def memory_usage_uss_pss
- sum_matches(PROC_SMAPS_ROLLUP_PATH, uss: PRIVATE_PAGES_PATTERN, pss: PSS_PATTERN)
+ # Returns the given process' USS/PSS (unique/proportional set size) in bytes.
+ def memory_usage_uss_pss(pid: 'self')
+ sum_matches(PROC_SMAPS_ROLLUP_PATH % pid, uss: PRIVATE_PAGES_PATTERN, pss: PSS_PATTERN)
.transform_values(&:kilobytes)
end
diff --git a/lib/gitlab/nav/top_nav_menu_builder.rb b/lib/gitlab/nav/top_nav_menu_builder.rb
index 721ae1889b8..dca3432a6a1 100644
--- a/lib/gitlab/nav/top_nav_menu_builder.rb
+++ b/lib/gitlab/nav/top_nav_menu_builder.rb
@@ -6,9 +6,15 @@ module Gitlab
def initialize
@primary = []
@secondary = []
+ @last_header_added = nil
end
- def add_primary_menu_item(**args)
+ def add_primary_menu_item(header: nil, **args)
+ if header && (header != @last_header_added)
+ add_menu_header(dest: @primary, title: header)
+ @last_header_added = header
+ end
+
add_menu_item(dest: @primary, **args)
end
@@ -30,6 +36,12 @@ module Gitlab
dest.push(item)
end
+
+ def add_menu_header(dest:, **args)
+ header = ::Gitlab::Nav::TopNavMenuHeader.build(**args)
+
+ dest.push(header)
+ end
end
end
end
diff --git a/lib/gitlab/nav/top_nav_menu_header.rb b/lib/gitlab/nav/top_nav_menu_header.rb
new file mode 100644
index 00000000000..520091dbd97
--- /dev/null
+++ b/lib/gitlab/nav/top_nav_menu_header.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Nav
+ class TopNavMenuHeader
+ def self.build(title:)
+ {
+ type: :header,
+ title: title
+ }
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/nav/top_nav_menu_item.rb b/lib/gitlab/nav/top_nav_menu_item.rb
index 4cb38e6bb9b..75eb0e8a264 100644
--- a/lib/gitlab/nav/top_nav_menu_item.rb
+++ b/lib/gitlab/nav/top_nav_menu_item.rb
@@ -11,6 +11,7 @@ module Gitlab
def self.build(id:, title:, active: false, icon: '', href: '', view: '', css_class: nil, data: nil, emoji: nil)
{
id: id,
+ type: :item,
title: title,
active: active,
icon: icon,
diff --git a/lib/gitlab/nav/top_nav_view_model_builder.rb b/lib/gitlab/nav/top_nav_view_model_builder.rb
index 11ca6a3a3ba..a8e25708107 100644
--- a/lib/gitlab/nav/top_nav_view_model_builder.rb
+++ b/lib/gitlab/nav/top_nav_view_model_builder.rb
@@ -42,11 +42,14 @@ module Gitlab
def build
menu = @menu_builder.build
+ hide_menu_text = Feature.enabled?(:new_navbar_layout)
+
menu.merge({
views: @views,
shortcuts: @shortcuts,
- activeTitle: _('Menu')
- })
+ menuTitle: (_('Menu') unless hide_menu_text),
+ menuTooltip: (_('Main menu') if hide_menu_text)
+ }.compact)
end
end
end
diff --git a/lib/gitlab/no_cache_headers.rb b/lib/gitlab/no_cache_headers.rb
index f80ca2c1369..2d03741480d 100644
--- a/lib/gitlab/no_cache_headers.rb
+++ b/lib/gitlab/no_cache_headers.rb
@@ -4,8 +4,8 @@ module Gitlab
module NoCacheHeaders
DEFAULT_GITLAB_NO_CACHE_HEADERS = {
'Cache-Control' => "#{ActionDispatch::Http::Cache::Response::DEFAULT_CACHE_CONTROL}, no-store, no-cache",
- 'Pragma' => 'no-cache', # HTTP 1.0 compatibility
- 'Expires' => 'Fri, 01 Jan 1990 00:00:00 GMT'
+ 'Pragma' => 'no-cache', # HTTP 1.0 compatibility
+ 'Expires' => 'Fri, 01 Jan 1990 00:00:00 GMT'
}.freeze
def no_cache_headers
diff --git a/lib/gitlab/pagination/gitaly_keyset_pager.rb b/lib/gitlab/pagination/gitaly_keyset_pager.rb
index 1f1061fe4f1..d4de2791195 100644
--- a/lib/gitlab/pagination/gitaly_keyset_pager.rb
+++ b/lib/gitlab/pagination/gitaly_keyset_pager.rb
@@ -38,7 +38,7 @@ module Gitlab
if finder.is_a?(BranchesFinder)
Feature.enabled?(:branch_list_keyset_pagination, project)
elsif finder.is_a?(TagsFinder)
- Feature.enabled?(:tag_list_keyset_pagination, project)
+ true
elsif finder.is_a?(::Repositories::TreeFinder)
Feature.enabled?(:repository_tree_gitaly_pagination, project)
else
@@ -52,7 +52,7 @@ module Gitlab
if finder.is_a?(BranchesFinder)
Feature.enabled?(:branch_list_keyset_pagination, project)
elsif finder.is_a?(TagsFinder)
- Feature.enabled?(:tag_list_keyset_pagination, project)
+ true
elsif finder.is_a?(::Repositories::TreeFinder)
Feature.enabled?(:repository_tree_gitaly_pagination, project)
else
diff --git a/lib/gitlab/pagination/keyset/column_order_definition.rb b/lib/gitlab/pagination/keyset/column_order_definition.rb
index 302e7b406b1..d1fe1d2dfc1 100644
--- a/lib/gitlab/pagination/keyset/column_order_definition.rb
+++ b/lib/gitlab/pagination/keyset/column_order_definition.rb
@@ -213,7 +213,7 @@ module Gitlab
attr_reader :reversed_order_expression, :nullable, :distinct
def calculate_reversed_order(order_expression)
- unless AREL_ORDER_CLASSES.has_key?(order_expression.class) # Arel can reverse simple orders
+ unless order_expression.is_a?(Arel::Nodes::Ordering)
raise "Couldn't determine reversed order for `#{order_expression}`, please provide the `reversed_order_expression` parameter."
end
@@ -229,10 +229,10 @@ module Gitlab
end
def parse_order_direction(order_expression, order_direction)
- transformed_order_direction = if order_direction.nil? && AREL_ORDER_CLASSES[order_expression.class]
- AREL_ORDER_CLASSES[order_expression.class]
- elsif order_direction.present?
+ transformed_order_direction = if order_direction.present?
order_direction.to_s.downcase.to_sym
+ elsif order_expression.is_a?(Arel::Nodes::Ordering)
+ AREL_ORDER_CLASSES[order_expression.class] || AREL_ORDER_CLASSES[order_expression.value.class]
end
unless REVERSED_ORDER_DIRECTIONS.has_key?(transformed_order_direction)
diff --git a/lib/gitlab/patch/sidekiq_cron_poller.rb b/lib/gitlab/patch/sidekiq_cron_poller.rb
new file mode 100644
index 00000000000..630c364d455
--- /dev/null
+++ b/lib/gitlab/patch/sidekiq_cron_poller.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+# Patch to address https://github.com/ondrejbartas/sidekiq-cron/issues/361
+# This restores the poll interval to v1.2.0 behavior
+# https://github.com/ondrejbartas/sidekiq-cron/blob/v1.2.0/lib/sidekiq/cron/poller.rb#L36-L38
+# This patch only applies to v1.4.0
+require 'sidekiq/cron/version'
+
+if Gem::Version.new(Sidekiq::Cron::VERSION) != Gem::Version.new('1.4.0')
+ raise 'New version of sidekiq-cron detected, please remove or update this patch'
+end
+
+module Gitlab
+ module Patch
+ module SidekiqCronPoller
+ def poll_interval_average
+ Sidekiq.options[:poll_interval] || Sidekiq::Cron::POLL_INTERVAL
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/quick_actions/issue_actions.rb b/lib/gitlab/quick_actions/issue_actions.rb
index 189627506f3..4883c649a62 100644
--- a/lib/gitlab/quick_actions/issue_actions.rb
+++ b/lib/gitlab/quick_actions/issue_actions.rb
@@ -207,19 +207,22 @@ module Gitlab
desc { _('Add Zoom meeting') }
explanation { _('Adds a Zoom meeting.') }
- params '<Zoom URL>'
+ params do
+ zoom_link_params
+ end
types Issue
condition do
@zoom_service = zoom_link_service
+
@zoom_service.can_add_link?
end
- parse_params do |link|
- @zoom_service.parse_link(link)
+ parse_params do |link_params|
+ @zoom_service.parse_link(link_params)
end
- command :zoom do |link|
- result = @zoom_service.add_link(link)
+ command :zoom do |link, link_text = nil|
+ result = add_zoom_link(link, link_text)
@execution_message[:zoom] = result.message
- @updates.merge!(result.payload) if result.payload
+ merge_updates(result, @updates)
end
desc { _('Remove Zoom meeting') }
@@ -315,12 +318,52 @@ module Gitlab
@updates[:remove_contacts] = contact_emails.split(' ')
end
- private
-
- def zoom_link_service
- ::Issues::ZoomLinkService.new(project: quick_action_target.project, current_user: current_user, params: { issue: quick_action_target })
+ desc { _('Add a timeline event to incident') }
+ explanation { _('Adds a timeline event to incident.') }
+ params '<timeline comment> | <date(YYYY-MM-DD)> <time(HH:MM)>'
+ types Issue
+ condition do
+ quick_action_target.incident? &&
+ current_user.can?(:admin_incident_management_timeline_event, quick_action_target)
+ end
+ parse_params do |event_params|
+ Gitlab::QuickActions::TimelineTextAndDateTimeSeparator.new(event_params).execute
+ end
+ command :timeline do |event_text, date_time|
+ if event_text && date_time
+ timeline_event = timeline_event_create_service(event_text, date_time).execute
+
+ @execution_message[:timeline] =
+ if timeline_event.success?
+ _('Timeline event added successfully.')
+ else
+ _('Something went wrong while adding timeline event.')
+ end
+ end
end
end
+
+ private
+
+ def zoom_link_service
+ ::Issues::ZoomLinkService.new(project: quick_action_target.project, current_user: current_user, params: { issue: quick_action_target })
+ end
+
+ def zoom_link_params
+ '<Zoom URL>'
+ end
+
+ def add_zoom_link(link, _link_text)
+ zoom_link_service.add_link(link)
+ end
+
+ def merge_updates(result, update_hash)
+ update_hash.merge!(result.payload) if result.payload
+ end
+
+ def timeline_event_create_service(event_text, event_date_time)
+ ::IncidentManagement::TimelineEvents::CreateService.new(quick_action_target, current_user, { note: event_text, occurred_at: event_date_time, editable: true })
+ end
end
end
end
diff --git a/lib/gitlab/quick_actions/merge_request_actions.rb b/lib/gitlab/quick_actions/merge_request_actions.rb
index 3cb01db1491..d38b81bff0b 100644
--- a/lib/gitlab/quick_actions/merge_request_actions.rb
+++ b/lib/gitlab/quick_actions/merge_request_actions.rb
@@ -88,33 +88,21 @@ module Gitlab
@execution_message[:rebase] = _('Scheduled a rebase of branch %{branch}.') % { branch: branch }
end
- desc { _('Toggle the Draft status') }
+ desc { _('Set the Draft status') }
explanation do
- noun = quick_action_target.to_ability_name.humanize(capitalize: false)
- if quick_action_target.draft?
- _("Marks this %{noun} as ready.")
- else
- _("Marks this %{noun} as a draft.")
- end % { noun: noun }
+ draft_action_message(_("Marks"))
end
execution_message do
- noun = quick_action_target.to_ability_name.humanize(capitalize: false)
- if quick_action_target.draft?
- _("Marked this %{noun} as ready.")
- else
- _("Marked this %{noun} as a draft.")
- end % { noun: noun }
+ draft_action_message(_("Marked"))
end
types MergeRequest
condition do
quick_action_target.respond_to?(:draft?) &&
- # Allow it to mark as draft on MR creation page or through MR notes
- #
(quick_action_target.new_record? || current_user.can?(:"update_#{quick_action_target.to_ability_name}", quick_action_target))
end
command :draft do
- @updates[:wip_event] = quick_action_target.draft? ? 'ready' : 'draft'
+ @updates[:wip_event] = draft_action_command
end
desc { _('Set the Ready status') }
@@ -317,6 +305,25 @@ module Gitlab
end
end
+ def draft_action_message(verb)
+ noun = quick_action_target.to_ability_name.humanize(capitalize: false)
+ if !quick_action_target.draft?
+ _("%{verb} this %{noun} as a draft.")
+ elsif Feature.disabled?(:draft_quick_action_non_toggle, quick_action_target.project)
+ _("%{verb} this %{noun} as ready.")
+ else
+ _("No change to this %{noun}'s draft status.")
+ end % { verb: verb, noun: noun }
+ end
+
+ def draft_action_command
+ if Feature.disabled?(:draft_quick_action_non_toggle, quick_action_target.project)
+ quick_action_target.draft? ? 'ready' : 'draft'
+ else
+ 'draft'
+ end
+ end
+
def merge_orchestration_service
@merge_orchestration_service ||= ::MergeRequests::MergeOrchestrationService.new(project, current_user)
end
diff --git a/lib/gitlab/quick_actions/timeline_text_and_date_time_separator.rb b/lib/gitlab/quick_actions/timeline_text_and_date_time_separator.rb
new file mode 100644
index 00000000000..e8002656ff5
--- /dev/null
+++ b/lib/gitlab/quick_actions/timeline_text_and_date_time_separator.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module QuickActions
+ class TimelineTextAndDateTimeSeparator
+ DATETIME_REGEX = %r{(\d{2,4}[\-.]\d{1,2}[\-.]\d{1,2} \d{1,2}:\d{2})}.freeze
+ MIXED_DELIMITER = %r{([/.])}.freeze
+ TIME_REGEX = %r{(\d{1,2}:\d{2})}.freeze
+
+ def initialize(timeline_event_arg)
+ @timeline_event_arg = timeline_event_arg
+ @timeline_text = get_text
+ @timeline_date_string = get_raw_date_string
+ end
+
+ def execute
+ return if @timeline_event_arg.blank?
+ return if date_contains_mixed_delimiters?
+ return [@timeline_text, get_current_date_time] unless date_time_present?
+ return unless valid_date?
+
+ [@timeline_text, get_actual_date_time]
+ end
+
+ private
+
+ def get_text
+ @timeline_event_arg.split('|')[0]&.strip
+ end
+
+ def get_raw_date_string
+ @timeline_event_arg.split('|')[1]&.strip
+ end
+
+ def get_current_date_time
+ DateTime.current.strftime("%Y-%m-%d %H:%M:00 UTC")
+ end
+
+ def get_actual_date_time
+ DateTime.parse(@timeline_date_string)
+ end
+
+ def date_time_present?
+ DATETIME_REGEX =~ @timeline_date_string || TIME_REGEX =~ @timeline_date_string
+ end
+
+ def date_contains_mixed_delimiters?
+ MIXED_DELIMITER =~ @timeline_date_string
+ end
+
+ def valid_date?
+ get_actual_date_time
+ rescue Date::Error
+ nil
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/reactive_cache_set_cache.rb b/lib/gitlab/reactive_cache_set_cache.rb
index 7ccbeadfd8a..2de3c07712f 100644
--- a/lib/gitlab/reactive_cache_set_cache.rb
+++ b/lib/gitlab/reactive_cache_set_cache.rb
@@ -15,8 +15,10 @@ module Gitlab
keys = read(key).map { |value| "#{cache_namespace}:#{value}" }
keys << cache_key(key)
- redis.pipelined do
- keys.each_slice(1000) { |subset| redis.unlink(*subset) }
+ Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
+ redis.pipelined do |pipeline|
+ keys.each_slice(1000) { |subset| pipeline.unlink(*subset) }
+ end
end
end
end
diff --git a/lib/gitlab/redis.rb b/lib/gitlab/redis.rb
new file mode 100644
index 00000000000..8857b544364
--- /dev/null
+++ b/lib/gitlab/redis.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Redis
+ # List all Gitlab::Redis::Wrapper descendants that are backed by an actual
+ # separate redis instance here.
+ #
+ # This will make sure the connection pool is initialized on application boot in
+ # config/initializers/7_redis.rb, instrumented, and used in health- & readiness checks.
+ ALL_CLASSES = [
+ Gitlab::Redis::Cache,
+ Gitlab::Redis::Queues,
+ Gitlab::Redis::RateLimiting,
+ Gitlab::Redis::Sessions,
+ Gitlab::Redis::SharedState,
+ Gitlab::Redis::TraceChunks
+ ].freeze
+ end
+end
diff --git a/lib/gitlab/redis/cache.rb b/lib/gitlab/redis/cache.rb
index 4ab1024d528..043f14630d5 100644
--- a/lib/gitlab/redis/cache.rb
+++ b/lib/gitlab/redis/cache.rb
@@ -12,7 +12,7 @@ module Gitlab
redis: pool,
compress: Gitlab::Utils.to_boolean(ENV.fetch('ENABLE_REDIS_CACHE_COMPRESSION', '1')),
namespace: CACHE_NAMESPACE,
- expires_in: ENV.fetch('GITLAB_RAILS_CACHE_DEFAULT_TTL_SECONDS', 2.weeks).to_i # Cache should not grow forever
+ expires_in: ENV.fetch('GITLAB_RAILS_CACHE_DEFAULT_TTL_SECONDS', 8.hours).to_i # Cache should not grow forever
}
end
end
diff --git a/lib/gitlab/redis/multi_store.rb b/lib/gitlab/redis/multi_store.rb
index cdd2ac6100e..a7c36786d2d 100644
--- a/lib/gitlab/redis/multi_store.rb
+++ b/lib/gitlab/redis/multi_store.rb
@@ -267,7 +267,7 @@ module Gitlab
def same_redis_store?
strong_memoize(:same_redis_store) do
- # <Redis client v4.4.0 for redis:///path_to/redis/redis.socket/5>"
+ # <Redis client v4.7.1 for unix:///path_to/redis/redis.socket/5>"
primary_store.inspect == secondary_store.inspect
end
end
diff --git a/lib/gitlab/repository_hash_cache.rb b/lib/gitlab/repository_hash_cache.rb
index 430f3e8d162..1ecdf506208 100644
--- a/lib/gitlab/repository_hash_cache.rb
+++ b/lib/gitlab/repository_hash_cache.rb
@@ -83,14 +83,14 @@ module Gitlab
full_key = cache_key(key)
with do |redis|
- results = redis.pipelined do
+ results = redis.pipelined do |pipeline|
# Set each hash key to the provided value
hash.each do |h_key, h_value|
- redis.hset(full_key, h_key, h_value)
+ pipeline.hset(full_key, h_key, h_value)
end
# Update the expiry time for this hset
- redis.expire(full_key, expires_in)
+ pipeline.expire(full_key, expires_in)
end
results.all?
diff --git a/lib/gitlab/repository_set_cache.rb b/lib/gitlab/repository_set_cache.rb
index 3061fb96190..33c7d96c45b 100644
--- a/lib/gitlab/repository_set_cache.rb
+++ b/lib/gitlab/repository_set_cache.rb
@@ -21,14 +21,14 @@ module Gitlab
full_key = cache_key(key)
with do |redis|
- redis.multi do
- redis.unlink(full_key)
+ redis.multi do |multi|
+ multi.unlink(full_key)
# Splitting into groups of 1000 prevents us from creating a too-long
# Redis command
- value.each_slice(1000) { |subset| redis.sadd(full_key, subset) }
+ value.each_slice(1000) { |subset| multi.sadd(full_key, subset) }
- redis.expire(full_key, expires_in)
+ multi.expire(full_key, expires_in)
end
end
@@ -39,9 +39,9 @@ module Gitlab
full_key = cache_key(key)
smembers, exists = with do |redis|
- redis.multi do
- redis.smembers(full_key)
- redis.exists(full_key)
+ redis.multi do |multi|
+ multi.smembers(full_key)
+ multi.exists(full_key)
end
end
diff --git a/lib/gitlab/request_forgery_protection.rb b/lib/gitlab/request_forgery_protection.rb
index a84a6ac2d14..258c904290d 100644
--- a/lib/gitlab/request_forgery_protection.rb
+++ b/lib/gitlab/request_forgery_protection.rb
@@ -6,6 +6,7 @@
module Gitlab
module RequestForgeryProtection
+ # rubocop:disable Rails/ApplicationController
class Controller < ActionController::Base
protect_from_forgery with: :exception, prepend: true
@@ -31,5 +32,6 @@ module Gitlab
rescue ActionController::InvalidAuthenticityToken
false
end
+ # rubocop:enable Rails/ApplicationController
end
end
diff --git a/lib/gitlab/seeder.rb b/lib/gitlab/seeder.rb
index 2450ad88bbb..ec514adafc8 100644
--- a/lib/gitlab/seeder.rb
+++ b/lib/gitlab/seeder.rb
@@ -151,48 +151,6 @@ module Gitlab
model.logger = old_loggers[connection_name]
end
end
-
- module Ci
- class DailyBuildGroupReportResult
- DEFAULT_BRANCH = 'master'
- COUNT_OF_DAYS = 5
-
- def initialize(project)
- @project = project
- @last_pipeline = project.last_pipeline
- end
-
- def seed
- COUNT_OF_DAYS.times do |count|
- date = Time.now.utc - count.day
- create_report(date)
- end
- end
-
- private
-
- attr_reader :project, :last_pipeline
-
- def create_report(date)
- last_pipeline.builds.uniq(&:group_name).each do |build|
- ::Ci::DailyBuildGroupReportResult.create(
- project: project,
- last_pipeline: last_pipeline,
- date: date,
- ref_path: last_pipeline.source_ref_path,
- group_name: build.group_name,
- data: {
- 'coverage' => rand(20..99)
- },
- group: project.group,
- default_branch: last_pipeline.default_branch?
- )
- rescue ActiveRecord::RecordNotUnique
- return false
- end
- end
- end
- end
end
end
# :nocov:
diff --git a/lib/gitlab/seeders/ci/daily_build_group_report_result.rb b/lib/gitlab/seeders/ci/daily_build_group_report_result.rb
new file mode 100644
index 00000000000..10ec65f6bf4
--- /dev/null
+++ b/lib/gitlab/seeders/ci/daily_build_group_report_result.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Seeders
+ module Ci
+ class DailyBuildGroupReportResult
+ DEFAULT_BRANCH = 'master'
+ COUNT_OF_DAYS = 5
+
+ def initialize(project)
+ @project = project
+ @last_pipeline = project.last_pipeline
+ end
+
+ def seed
+ COUNT_OF_DAYS.times do |count|
+ date = Time.now.utc - count.day
+ create_report(date)
+ end
+ end
+
+ private
+
+ attr_reader :project, :last_pipeline
+
+ def create_report(date)
+ last_pipeline.builds.uniq(&:group_name).each do |build|
+ ::Ci::DailyBuildGroupReportResult.create(
+ project: project,
+ last_pipeline: last_pipeline,
+ date: date,
+ ref_path: last_pipeline.source_ref_path,
+ group_name: build.group_name,
+ data: {
+ 'coverage' => rand(20..99)
+ },
+ group: project.group,
+ default_branch: last_pipeline.default_branch?
+ )
+ rescue ActiveRecord::RecordNotUnique
+ return false
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/set_cache.rb b/lib/gitlab/set_cache.rb
index 896e7e3f65e..23c23393bc8 100644
--- a/lib/gitlab/set_cache.rb
+++ b/lib/gitlab/set_cache.rb
@@ -33,10 +33,10 @@ module Gitlab
def write(key, value)
with do |redis|
- redis.pipelined do
- redis.sadd(cache_key(key), value)
+ redis.pipelined do |pipeline|
+ pipeline.sadd(cache_key(key), value)
- redis.expire(cache_key(key), expires_in)
+ pipeline.expire(cache_key(key), expires_in)
end
end
@@ -57,9 +57,9 @@ module Gitlab
full_key = cache_key(key)
with do |redis|
- redis.multi do
- redis.sismember(full_key, value)
- redis.exists(full_key)
+ redis.multi do |multi|
+ multi.sismember(full_key, value)
+ multi.exists(full_key)
end
end
end
diff --git a/lib/gitlab/shell.rb b/lib/gitlab/shell.rb
index d26e1a34a9f..b167afe589a 100644
--- a/lib/gitlab/shell.rb
+++ b/lib/gitlab/shell.rb
@@ -70,7 +70,9 @@ module Gitlab
link_path = File.join(shell_path, '.gitlab_shell_secret')
if File.exist?(shell_path) && !File.exist?(link_path)
- FileUtils.symlink(secret_file, link_path)
+ # It could happen that link_path is a broken symbolic link.
+ # In that case !File.exist?(link_path) is true, but we still want to overwrite the (broken) symbolic link.
+ FileUtils.ln_sf(secret_file, link_path)
end
end
end
diff --git a/lib/gitlab/sidekiq_daemon/memory_killer.rb b/lib/gitlab/sidekiq_daemon/memory_killer.rb
index ca92fed9c40..24e2eca420e 100644
--- a/lib/gitlab/sidekiq_daemon/memory_killer.rb
+++ b/lib/gitlab/sidekiq_daemon/memory_killer.rb
@@ -41,11 +41,11 @@ module Gitlab
def init_metrics
{
- sidekiq_current_rss: ::Gitlab::Metrics.gauge(:sidekiq_current_rss, 'Current RSS of Sidekiq Worker'),
+ sidekiq_current_rss: ::Gitlab::Metrics.gauge(:sidekiq_current_rss, 'Current RSS of Sidekiq Worker'),
sidekiq_memory_killer_soft_limit_rss: ::Gitlab::Metrics.gauge(:sidekiq_memory_killer_soft_limit_rss, 'Current soft_limit_rss of Sidekiq Worker'),
sidekiq_memory_killer_hard_limit_rss: ::Gitlab::Metrics.gauge(:sidekiq_memory_killer_hard_limit_rss, 'Current hard_limit_rss of Sidekiq Worker'),
- sidekiq_memory_killer_phase: ::Gitlab::Metrics.gauge(:sidekiq_memory_killer_phase, 'Current phase of Sidekiq Worker'),
- sidekiq_memory_killer_running_jobs: ::Gitlab::Metrics.counter(:sidekiq_memory_killer_running_jobs_total, 'Current running jobs when limit was reached')
+ sidekiq_memory_killer_phase: ::Gitlab::Metrics.gauge(:sidekiq_memory_killer_phase, 'Current phase of Sidekiq Worker'),
+ sidekiq_memory_killer_running_jobs: ::Gitlab::Metrics.counter(:sidekiq_memory_killer_running_jobs_total, 'Current running jobs when limit was reached')
}
end
diff --git a/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job.rb b/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job.rb
index 7533770e254..ab126ea4749 100644
--- a/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job.rb
+++ b/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job.rb
@@ -112,10 +112,12 @@ module Gitlab
end
def delete!
- with_redis do |redis|
- redis.multi do |multi|
- multi.del(idempotency_key, deduplicated_flag_key)
- delete_wal_locations!(multi)
+ Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
+ with_redis do |redis|
+ redis.multi do |multi|
+ multi.del(idempotency_key, deduplicated_flag_key)
+ delete_wal_locations!(multi)
+ end
end
end
end
diff --git a/lib/gitlab/sidekiq_middleware/server_metrics.rb b/lib/gitlab/sidekiq_middleware/server_metrics.rb
index 180cdad916b..3dd5355d3a3 100644
--- a/lib/gitlab/sidekiq_middleware/server_metrics.rb
+++ b/lib/gitlab/sidekiq_middleware/server_metrics.rb
@@ -22,21 +22,21 @@ module Gitlab
def metrics
{
- sidekiq_jobs_cpu_seconds: ::Gitlab::Metrics.histogram(:sidekiq_jobs_cpu_seconds, 'Seconds this Sidekiq job spent on the CPU', {}, SIDEKIQ_LATENCY_BUCKETS),
- sidekiq_jobs_completion_seconds: ::Gitlab::Metrics.histogram(:sidekiq_jobs_completion_seconds, 'Seconds to complete Sidekiq job', {}, SIDEKIQ_JOB_DURATION_BUCKETS),
- sidekiq_jobs_db_seconds: ::Gitlab::Metrics.histogram(:sidekiq_jobs_db_seconds, 'Seconds of database time to run Sidekiq job', {}, SIDEKIQ_LATENCY_BUCKETS),
- sidekiq_jobs_gitaly_seconds: ::Gitlab::Metrics.histogram(:sidekiq_jobs_gitaly_seconds, 'Seconds of Gitaly time to run Sidekiq job', {}, SIDEKIQ_LATENCY_BUCKETS),
- sidekiq_jobs_queue_duration_seconds: ::Gitlab::Metrics.histogram(:sidekiq_jobs_queue_duration_seconds, 'Duration in seconds that a Sidekiq job was queued before being executed', {}, SIDEKIQ_QUEUE_DURATION_BUCKETS),
+ sidekiq_jobs_cpu_seconds: ::Gitlab::Metrics.histogram(:sidekiq_jobs_cpu_seconds, 'Seconds this Sidekiq job spent on the CPU', {}, SIDEKIQ_LATENCY_BUCKETS),
+ sidekiq_jobs_completion_seconds: ::Gitlab::Metrics.histogram(:sidekiq_jobs_completion_seconds, 'Seconds to complete Sidekiq job', {}, SIDEKIQ_JOB_DURATION_BUCKETS),
+ sidekiq_jobs_db_seconds: ::Gitlab::Metrics.histogram(:sidekiq_jobs_db_seconds, 'Seconds of database time to run Sidekiq job', {}, SIDEKIQ_LATENCY_BUCKETS),
+ sidekiq_jobs_gitaly_seconds: ::Gitlab::Metrics.histogram(:sidekiq_jobs_gitaly_seconds, 'Seconds of Gitaly time to run Sidekiq job', {}, SIDEKIQ_LATENCY_BUCKETS),
+ sidekiq_jobs_queue_duration_seconds: ::Gitlab::Metrics.histogram(:sidekiq_jobs_queue_duration_seconds, 'Duration in seconds that a Sidekiq job was queued before being executed', {}, SIDEKIQ_QUEUE_DURATION_BUCKETS),
sidekiq_redis_requests_duration_seconds: ::Gitlab::Metrics.histogram(:sidekiq_redis_requests_duration_seconds, 'Duration in seconds that a Sidekiq job spent requests a Redis server', {}, Gitlab::Instrumentation::Redis::QUERY_TIME_BUCKETS),
sidekiq_elasticsearch_requests_duration_seconds: ::Gitlab::Metrics.histogram(:sidekiq_elasticsearch_requests_duration_seconds, 'Duration in seconds that a Sidekiq job spent in requests to an Elasticsearch server', {}, SIDEKIQ_LATENCY_BUCKETS),
- sidekiq_jobs_failed_total: ::Gitlab::Metrics.counter(:sidekiq_jobs_failed_total, 'Sidekiq jobs failed'),
- sidekiq_jobs_retried_total: ::Gitlab::Metrics.counter(:sidekiq_jobs_retried_total, 'Sidekiq jobs retried'),
- sidekiq_jobs_interrupted_total: ::Gitlab::Metrics.counter(:sidekiq_jobs_interrupted_total, 'Sidekiq jobs interrupted'),
- sidekiq_redis_requests_total: ::Gitlab::Metrics.counter(:sidekiq_redis_requests_total, 'Redis requests during a Sidekiq job execution'),
- sidekiq_elasticsearch_requests_total: ::Gitlab::Metrics.counter(:sidekiq_elasticsearch_requests_total, 'Elasticsearch requests during a Sidekiq job execution'),
- sidekiq_running_jobs: ::Gitlab::Metrics.gauge(:sidekiq_running_jobs, 'Number of Sidekiq jobs running', {}, :all),
- sidekiq_concurrency: ::Gitlab::Metrics.gauge(:sidekiq_concurrency, 'Maximum number of Sidekiq jobs', {}, :all),
- sidekiq_mem_total_bytes: ::Gitlab::Metrics.gauge(:sidekiq_mem_total_bytes, 'Number of bytes allocated for both objects consuming an object slot and objects that required a malloc', {}, :all)
+ sidekiq_jobs_failed_total: ::Gitlab::Metrics.counter(:sidekiq_jobs_failed_total, 'Sidekiq jobs failed'),
+ sidekiq_jobs_retried_total: ::Gitlab::Metrics.counter(:sidekiq_jobs_retried_total, 'Sidekiq jobs retried'),
+ sidekiq_jobs_interrupted_total: ::Gitlab::Metrics.counter(:sidekiq_jobs_interrupted_total, 'Sidekiq jobs interrupted'),
+ sidekiq_redis_requests_total: ::Gitlab::Metrics.counter(:sidekiq_redis_requests_total, 'Redis requests during a Sidekiq job execution'),
+ sidekiq_elasticsearch_requests_total: ::Gitlab::Metrics.counter(:sidekiq_elasticsearch_requests_total, 'Elasticsearch requests during a Sidekiq job execution'),
+ sidekiq_running_jobs: ::Gitlab::Metrics.gauge(:sidekiq_running_jobs, 'Number of Sidekiq jobs running', {}, :all),
+ sidekiq_concurrency: ::Gitlab::Metrics.gauge(:sidekiq_concurrency, 'Maximum number of Sidekiq jobs', {}, :all),
+ sidekiq_mem_total_bytes: ::Gitlab::Metrics.gauge(:sidekiq_mem_total_bytes, 'Number of bytes allocated for both objects consuming an object slot and objects that required a malloc', {}, :all)
}
end
diff --git a/lib/gitlab/sidekiq_versioning.rb b/lib/gitlab/sidekiq_versioning.rb
index 80c0b7650f3..28c9714f82f 100644
--- a/lib/gitlab/sidekiq_versioning.rb
+++ b/lib/gitlab/sidekiq_versioning.rb
@@ -10,11 +10,7 @@ module Gitlab
if queues.any?
Sidekiq.redis do |conn|
- conn.pipelined do
- queues.each do |queue|
- conn.sadd('queues', queue)
- end
- end
+ conn.sadd('queues', queues)
end
end
rescue ::Redis::BaseError, SocketError, Errno::ENOENT, Errno::EADDRNOTAVAIL, Errno::EAFNOSUPPORT, Errno::ECONNRESET, Errno::ECONNREFUSED
diff --git a/lib/gitlab/slash_commands/presenters/base.rb b/lib/gitlab/slash_commands/presenters/base.rb
index d28b5fb509a..55497c5e365 100644
--- a/lib/gitlab/slash_commands/presenters/base.rb
+++ b/lib/gitlab/slash_commands/presenters/base.rb
@@ -87,16 +87,16 @@ module Gitlab
{
attachments: [
{
- title: "#{issue.title} · #{issue.to_reference}",
- title_link: resource_url,
- author_name: author.name,
- author_icon: author.avatar_url(only_path: false),
- fallback: fallback_message,
- pretext: custom_pretext,
- text: text,
- color: color(resource),
- fields: fields,
- mrkdwn_in: fields_with_markdown
+ title: "#{issue.title} · #{issue.to_reference}",
+ title_link: resource_url,
+ author_name: author.name,
+ author_icon: author.avatar_url(only_path: false),
+ fallback: fallback_message,
+ pretext: custom_pretext,
+ text: text,
+ color: color(resource),
+ fields: fields,
+ mrkdwn_in: fields_with_markdown
}
]
}
diff --git a/lib/gitlab/spamcheck/client.rb b/lib/gitlab/spamcheck/client.rb
index 40b01552244..0b9f3baa4de 100644
--- a/lib/gitlab/spamcheck/client.rb
+++ b/lib/gitlab/spamcheck/client.rb
@@ -33,33 +33,50 @@ module Gitlab
@endpoint_url = @endpoint_url.sub(URL_SCHEME_REGEX, '')
end
- def issue_spam?(spam_issue:, user:, context: {})
- issue = build_issue_protobuf(issue: spam_issue, user: user, context: context)
+ def spam?(spammable:, user:, context: {}, extra_features: {})
+ metadata = { 'authorization' => Gitlab::CurrentSettings.spam_check_api_key || '' }
+ protobuf_args = { spammable: spammable, user: user, context: context, extra_features: extra_features }
+
+ pb, grpc_method = build_protobuf(**protobuf_args)
+ response = grpc_method.call(pb, metadata: metadata)
- response = grpc_client.check_for_spam_issue(issue,
- metadata: { 'authorization' =>
- Gitlab::CurrentSettings.spam_check_api_key })
verdict = convert_verdict_to_gitlab_constant(response.verdict)
[verdict, response.extra_attributes.to_h, response.error]
end
private
+ def get_spammable_mappings(spammable)
+ case spammable
+ when Issue
+ [::Spamcheck::Issue, grpc_client.method(:check_for_spam_issue)]
+ when Snippet
+ [::Spamcheck::Snippet, grpc_client.method(:check_for_spam_snippet)]
+ else
+ raise ArgumentError, "Not a spammable type: #{spammable.class.name}"
+ end
+ end
+
def convert_verdict_to_gitlab_constant(verdict)
VERDICT_MAPPING.fetch(::Spamcheck::SpamVerdict::Verdict.resolve(verdict), verdict)
end
- def build_issue_protobuf(issue:, user:, context:)
- issue_pb = ::Spamcheck::Issue.new
- issue_pb.title = issue.spam_title || ''
- issue_pb.description = issue.spam_description || ''
- issue_pb.created_at = convert_to_pb_timestamp(issue.created_at) if issue.created_at
- issue_pb.updated_at = convert_to_pb_timestamp(issue.updated_at) if issue.updated_at
- issue_pb.user_in_project = user.authorized_project?(issue.project)
- issue_pb.project = build_project_protobuf(issue)
- issue_pb.action = ACTION_MAPPING.fetch(context.fetch(:action)) if context.has_key?(:action)
- issue_pb.user = build_user_protobuf(user)
- issue_pb
+ def build_protobuf(spammable:, user:, context:, extra_features:)
+ protobuf_class, grpc_method = get_spammable_mappings(spammable)
+ pb = protobuf_class.new(**extra_features)
+ pb.title = spammable.spam_title || ''
+ pb.description = spammable.spam_description || ''
+ pb.created_at = convert_to_pb_timestamp(spammable.created_at) if spammable.created_at
+ pb.updated_at = convert_to_pb_timestamp(spammable.updated_at) if spammable.updated_at
+ pb.action = ACTION_MAPPING.fetch(context.fetch(:action)) if context.has_key?(:action)
+ pb.user = build_user_protobuf(user)
+
+ unless spammable.project.nil?
+ pb.user_in_project = user.authorized_project?(spammable.project)
+ pb.project = build_project_protobuf(spammable)
+ end
+
+ [pb, grpc_method]
end
def build_user_protobuf(user)
diff --git a/lib/gitlab/subscription_portal.rb b/lib/gitlab/subscription_portal.rb
index 7ef1be6ff44..7494f0584d0 100644
--- a/lib/gitlab/subscription_portal.rb
+++ b/lib/gitlab/subscription_portal.rb
@@ -22,6 +22,10 @@ module Gitlab
"payment_method_validation"
end
+ def self.registration_validation_form_id
+ "cc_registration_validation"
+ end
+
def self.registration_validation_form_url
"#{self.subscriptions_url}/payment_forms/cc_registration_validation"
end
@@ -90,3 +94,4 @@ Gitlab::SubscriptionPortal::PAYMENT_FORM_URL = Gitlab::SubscriptionPortal.paymen
Gitlab::SubscriptionPortal::PAYMENT_VALIDATION_FORM_ID = Gitlab::SubscriptionPortal.payment_validation_form_id.freeze
Gitlab::SubscriptionPortal::RENEWAL_SERVICE_EMAIL = Gitlab::SubscriptionPortal.renewal_service_email.freeze
Gitlab::SubscriptionPortal::REGISTRATION_VALIDATION_FORM_URL = Gitlab::SubscriptionPortal.registration_validation_form_url.freeze
+Gitlab::SubscriptionPortal::REGISTRATION_VALIDATION_FORM_ID = Gitlab::SubscriptionPortal.registration_validation_form_id.freeze
diff --git a/lib/gitlab/template/gitignore_template.rb b/lib/gitlab/template/gitignore_template.rb
index 72a1b7460c2..d8e0ec82410 100644
--- a/lib/gitlab/template/gitignore_template.rb
+++ b/lib/gitlab/template/gitignore_template.rb
@@ -11,7 +11,7 @@ module Gitlab
def categories
{
"Languages" => '',
- "Global" => 'Global'
+ "Global" => 'Global'
}
end
diff --git a/lib/gitlab/tracking.rb b/lib/gitlab/tracking.rb
index 3b46b4c5498..45f836f10d3 100644
--- a/lib/gitlab/tracking.rb
+++ b/lib/gitlab/tracking.rb
@@ -10,6 +10,8 @@ module Gitlab
def event(category, action, label: nil, property: nil, value: nil, context: [], project: nil, user: nil, namespace: nil, **extra) # rubocop:disable Metrics/ParameterLists
contexts = [Tracking::StandardContext.new(project: project, user: user, namespace: namespace, **extra).to_context, *context]
+ action = action.to_s
+
tracker.event(category, action, label: label, property: property, value: value, context: contexts)
rescue StandardError => error
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(error, snowplow_category: category, snowplow_action: action)
diff --git a/lib/gitlab/tree_summary.rb b/lib/gitlab/tree_summary.rb
index 72df8b423df..ba3176ca6e7 100644
--- a/lib/gitlab/tree_summary.rb
+++ b/lib/gitlab/tree_summary.rb
@@ -6,7 +6,7 @@ module Gitlab
include ::MarkupHelper
CACHE_EXPIRE_IN = 1.hour
- MAX_OFFSET = 2**31
+ MAX_OFFSET = 2**31 - 1
attr_reader :commit, :project, :path, :offset, :limit, :user, :resolved_commits
@@ -35,6 +35,8 @@ module Gitlab
# - commit_path: URI of the commit in the web interface
# - commit_title_html: Rendered commit title
def summarize
+ return [] if offset < 0
+
commits_hsh = fetch_last_cached_commits_list
prerender_commit_full_titles!(commits_hsh.values)
diff --git a/lib/gitlab/uploads/migration_helper.rb b/lib/gitlab/uploads/migration_helper.rb
index deab2cd43a6..712512d0e02 100644
--- a/lib/gitlab/uploads/migration_helper.rb
+++ b/lib/gitlab/uploads/migration_helper.rb
@@ -5,27 +5,10 @@ module Gitlab
class MigrationHelper
attr_reader :logger
- CATEGORIES = [%w(AvatarUploader Project :avatar),
- %w(AvatarUploader Group :avatar),
- %w(AvatarUploader User :avatar),
- %w(AttachmentUploader Note :attachment),
- %w(AttachmentUploader Appearance :logo),
- %w(AttachmentUploader Appearance :header_logo),
- %w(FaviconUploader Appearance :favicon),
- %w(FileUploader Project),
- %w(PersonalFileUploader Snippet),
- %w(NamespaceFileUploader Snippet),
- %w(DesignManagement::DesignV432x230Uploader DesignManagement::Action :image_v432x230),
- %w(FileUploader MergeRequest)].freeze
-
def initialize(args, logger)
prepare_variables(args, logger)
end
- def self.categories
- CATEGORIES
- end
-
def migrate_to_remote_storage
@to_store = ObjectStorage::Store::REMOTE
@@ -45,17 +28,14 @@ module Gitlab
end
def prepare_variables(args, logger)
- @mounted_as = args.mounted_as&.gsub(':', '')&.to_sym
- @uploader_class = args.uploader_class.constantize
- @model_class = args.model_class.constantize
+ @mounted_as = args.mounted_as&.gsub(':', '')
+ @uploader_class = args.uploader_class
+ @model_class = args.model_class&.constantize
@logger = logger
end
def enqueue_batch(batch, index)
- job = ObjectStorage::MigrateUploadsWorker.enqueue!(batch,
- @model_class,
- @mounted_as,
- @to_store)
+ job = ObjectStorage::MigrateUploadsWorker.enqueue!(batch, @to_store)
logger.info(message: "[Uploads migration] Enqueued upload migration job", index: index, job_id: job)
rescue ObjectStorage::MigrateUploadsWorker::SanityCheckError => e
# continue for the next batch
@@ -66,10 +46,12 @@ module Gitlab
def uploads(store_type = [nil, ObjectStorage::Store::LOCAL])
Upload.class_eval { include EachBatch } unless Upload < EachBatch
- Upload
- .where(store: store_type,
- uploader: @uploader_class.to_s,
- model_type: @model_class.base_class.sti_name)
+ uploads = Upload.where(store: store_type)
+ uploads = uploads.where(uploader: @uploader_class) if @uploader_class.present?
+ uploads = uploads.where(model_type: @model_class.base_class.sti_name) if @model_class.present?
+ uploads = uploads.where(mount_point: @mounted_as) if @mounted_as.present?
+
+ uploads
end
# rubocop:enable CodeReuse/ActiveRecord
end
diff --git a/lib/gitlab/usage/metrics/instrumentations/count_bulk_imports_entities_metric.rb b/lib/gitlab/usage/metrics/instrumentations/count_bulk_imports_entities_metric.rb
index c0d53b1b21a..67dc1455b23 100644
--- a/lib/gitlab/usage/metrics/instrumentations/count_bulk_imports_entities_metric.rb
+++ b/lib/gitlab/usage/metrics/instrumentations/count_bulk_imports_entities_metric.rb
@@ -20,15 +20,20 @@ module Gitlab
private
def relation
- return super.where(source_type: source_type) if source_type.present? # rubocop: disable CodeReuse/ActiveRecord
-
- super
+ scope = super
+ scope = scope.where(source_type: source_type) if source_type.present?
+ scope = scope.where(status: status) if status.present?
+ scope
end
def source_type
options[:source_type].to_s
end
+ def status
+ options[:status]
+ end
+
def allowed_source_types
BulkImports::Entity.source_types.keys.map(&:to_s)
end
diff --git a/lib/gitlab/usage/metrics/instrumentations/count_user_auth_metric.rb b/lib/gitlab/usage/metrics/instrumentations/count_user_auth_metric.rb
new file mode 100644
index 00000000000..1de93ce6dfa
--- /dev/null
+++ b/lib/gitlab/usage/metrics/instrumentations/count_user_auth_metric.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Usage
+ module Metrics
+ module Instrumentations
+ class CountUserAuthMetric < DatabaseMetric
+ operation :distinct_count, column: :user_id
+
+ relation do
+ AuthenticationEvent.success
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/usage/metrics/instrumentations/redis_metric.rb b/lib/gitlab/usage/metrics/instrumentations/redis_metric.rb
index a25bad2436b..26d963e2407 100644
--- a/lib/gitlab/usage/metrics/instrumentations/redis_metric.rb
+++ b/lib/gitlab/usage/metrics/instrumentations/redis_metric.rb
@@ -11,37 +11,49 @@ module Gitlab
# instrumentation_class: RedisMetric
# options:
# event: pushes
- # counter_class: SourceCodeCounter
+ # prefix: source_code
#
class RedisMetric < BaseMetric
+ include Gitlab::UsageDataCounters::RedisCounter
+
+ USAGE_PREFIX = "USAGE_"
+
def initialize(time_frame:, options: {})
super
raise ArgumentError, "'event' option is required" unless metric_event.present?
- raise ArgumentError, "'counter class' option is required" unless counter_class.present?
+ raise ArgumentError, "'prefix' option is required" unless prefix.present?
end
def metric_event
options[:event]
end
- def counter_class_name
- options[:counter_class]
+ def prefix
+ options[:prefix]
end
- def counter_class
- "Gitlab::UsageDataCounters::#{counter_class_name}".constantize
+ def include_usage_prefix?
+ options.fetch(:include_usage_prefix, true)
end
def value
redis_usage_data do
- counter_class.read(metric_event)
+ total_count(redis_key)
end
end
def suggested_name
Gitlab::Usage::Metrics::NameSuggestion.for(:redis)
end
+
+ private
+
+ def redis_key
+ key = "#{prefix}_#{metric_event}".upcase
+ key.prepend(USAGE_PREFIX) if include_usage_prefix?
+ key
+ end
end
end
end
diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb
index 6f36a09fe48..e2232dc5e2a 100644
--- a/lib/gitlab/usage_data.rb
+++ b/lib/gitlab/usage_data.rb
@@ -137,7 +137,7 @@ module Gitlab
projects_with_error_tracking_enabled: count(::ErrorTracking::ProjectErrorTrackingSetting.where(enabled: true)),
projects_with_alerts_created: distinct_count(::AlertManagement::Alert, :project_id),
projects_with_enabled_alert_integrations: distinct_count(::AlertManagement::HttpIntegration.active, :project_id),
- projects_with_terraform_reports: distinct_count(::Ci::JobArtifact.terraform_reports, :project_id),
+ projects_with_terraform_reports: distinct_count(::Ci::JobArtifact.of_report_type(:terraform), :project_id),
projects_with_terraform_states: distinct_count(::Terraform::State, :project_id),
protected_branches: count(ProtectedBranch),
protected_branches_except_default: count(ProtectedBranch.where.not(name: ['main', 'master', Gitlab::CurrentSettings.default_branch_name])),
@@ -146,7 +146,7 @@ module Gitlab
personal_snippets: count(PersonalSnippet),
project_snippets: count(ProjectSnippet),
suggestions: count(Suggestion),
- terraform_reports: count(::Ci::JobArtifact.terraform_reports),
+ terraform_reports: count(::Ci::JobArtifact.of_report_type(:terraform)),
terraform_states: count(::Terraform::State),
todos: count(Todo),
uploads: count(Upload),
@@ -268,7 +268,7 @@ module Gitlab
# @return [Array<#totals>] An array of objects that respond to `#totals`
def usage_data_counters
- Gitlab::UsageDataCounters.counters
+ Gitlab::UsageDataCounters.unmigrated_counters
end
def components_usage_data
diff --git a/lib/gitlab/usage_data_counters.rb b/lib/gitlab/usage_data_counters.rb
index 224897ed758..eae1c593a8f 100644
--- a/lib/gitlab/usage_data_counters.rb
+++ b/lib/gitlab/usage_data_counters.rb
@@ -3,29 +3,38 @@
module Gitlab
module UsageDataCounters
COUNTERS = [
- PackageEventCounter,
WikiPageCounter,
- WebIdeCounter,
NoteCounter,
SnippetCounter,
SearchCounter,
CycleAnalyticsCounter,
ProductivityAnalyticsCounter,
SourceCodeCounter,
+ KubernetesAgentCounter,
+ MergeRequestWidgetExtensionCounter
+ ].freeze
+
+ COUNTERS_MIGRATED_TO_INSTRUMENTATION_CLASSES = [
+ PackageEventCounter,
MergeRequestCounter,
DesignsCounter,
- KubernetesAgentCounter,
DiffsCounter,
ServiceUsageDataCounter,
- MergeRequestWidgetExtensionCounter
+ WebIdeCounter
].freeze
UsageDataCounterError = Class.new(StandardError)
UnknownEvent = Class.new(UsageDataCounterError)
class << self
+ def unmigrated_counters
+ # we are using the #counters method instead of the COUNTERS const
+ # to make sure it's working correctly for `ee` version of UsageDataCounters
+ counters - self::COUNTERS_MIGRATED_TO_INSTRUMENTATION_CLASSES
+ end
+
def counters
- self::COUNTERS
+ self::COUNTERS + self::COUNTERS_MIGRATED_TO_INSTRUMENTATION_CLASSES
end
def count(event_name)
diff --git a/lib/gitlab/usage_data_counters/base_counter.rb b/lib/gitlab/usage_data_counters/base_counter.rb
index 4ab310a2519..5d2ab5eaf74 100644
--- a/lib/gitlab/usage_data_counters/base_counter.rb
+++ b/lib/gitlab/usage_data_counters/base_counter.rb
@@ -10,7 +10,9 @@ module Gitlab::UsageDataCounters
def redis_key(event)
require_known_event(event)
- "USAGE_#{prefix}_#{event}".upcase
+ usage_prefix = Gitlab::Usage::Metrics::Instrumentations::RedisMetric::USAGE_PREFIX
+
+ "#{usage_prefix}#{prefix}_#{event}".upcase
end
def count(event)
diff --git a/lib/gitlab/usage_data_counters/hll_redis_counter.rb b/lib/gitlab/usage_data_counters/hll_redis_counter.rb
index a5db8ba4dcc..f0cb9bcbe94 100644
--- a/lib/gitlab/usage_data_counters/hll_redis_counter.rb
+++ b/lib/gitlab/usage_data_counters/hll_redis_counter.rb
@@ -20,11 +20,7 @@ module Gitlab
CATEGORIES_FOR_TOTALS = %w[
analytics
- code_review
compliance
- deploy_token_packages
- ecosystem
- epic_boards_usage
epics_usage
error_tracking
ide_edit
@@ -32,11 +28,13 @@ module Gitlab
issues_edit
pipeline_authoring
quickactions
- user_packages
].freeze
CATEGORIES_COLLECTED_FROM_METRICS_DEFINITIONS = %w[
ci_users
+ deploy_token_packages
+ code_review
+ ecosystem
error_tracking
ide_edit
importer
@@ -49,6 +47,7 @@ module Gitlab
source_code
terraform
testing
+ user_packages
work_items
].freeze
diff --git a/lib/gitlab/usage_data_counters/issue_activity_unique_counter.rb b/lib/gitlab/usage_data_counters/issue_activity_unique_counter.rb
index 316d9bb3dc1..dda72f7fa3b 100644
--- a/lib/gitlab/usage_data_counters/issue_activity_unique_counter.rb
+++ b/lib/gitlab/usage_data_counters/issue_activity_unique_counter.rb
@@ -36,95 +36,118 @@ module Gitlab
ISSUE_COMMENT_REMOVED = 'g_project_management_issue_comment_removed'
class << self
- def track_issue_created_action(author:)
+ def track_issue_created_action(author:, project:)
+ track_snowplow_action(ISSUE_CREATED, author, project)
track_unique_action(ISSUE_CREATED, author)
end
- def track_issue_title_changed_action(author:)
+ def track_issue_title_changed_action(author:, project:)
+ track_snowplow_action(ISSUE_TITLE_CHANGED, author, project)
track_unique_action(ISSUE_TITLE_CHANGED, author)
end
- def track_issue_description_changed_action(author:)
+ def track_issue_description_changed_action(author:, project:)
+ track_snowplow_action(ISSUE_DESCRIPTION_CHANGED, author, project)
track_unique_action(ISSUE_DESCRIPTION_CHANGED, author)
end
- def track_issue_assignee_changed_action(author:)
+ def track_issue_assignee_changed_action(author:, project:)
+ track_snowplow_action(ISSUE_ASSIGNEE_CHANGED, author, project)
track_unique_action(ISSUE_ASSIGNEE_CHANGED, author)
end
- def track_issue_made_confidential_action(author:)
+ def track_issue_made_confidential_action(author:, project:)
+ track_snowplow_action(ISSUE_MADE_CONFIDENTIAL, author, project)
track_unique_action(ISSUE_MADE_CONFIDENTIAL, author)
end
- def track_issue_made_visible_action(author:)
+ def track_issue_made_visible_action(author:, project:)
+ track_snowplow_action(ISSUE_MADE_VISIBLE, author, project)
track_unique_action(ISSUE_MADE_VISIBLE, author)
end
- def track_issue_closed_action(author:)
+ def track_issue_closed_action(author:, project:)
+ track_snowplow_action(ISSUE_CLOSED, author, project)
track_unique_action(ISSUE_CLOSED, author)
end
- def track_issue_reopened_action(author:)
+ def track_issue_reopened_action(author:, project:)
+ track_snowplow_action(ISSUE_REOPENED, author, project)
track_unique_action(ISSUE_REOPENED, author)
end
- def track_issue_label_changed_action(author:)
+ def track_issue_label_changed_action(author:, project:)
+ track_snowplow_action(ISSUE_LABEL_CHANGED, author, project)
track_unique_action(ISSUE_LABEL_CHANGED, author)
end
- def track_issue_milestone_changed_action(author:)
+ def track_issue_milestone_changed_action(author:, project:)
+ track_snowplow_action(ISSUE_MILESTONE_CHANGED, author, project)
track_unique_action(ISSUE_MILESTONE_CHANGED, author)
end
- def track_issue_cross_referenced_action(author:)
+ def track_issue_cross_referenced_action(author:, project:)
+ track_snowplow_action(ISSUE_CROSS_REFERENCED, author, project)
track_unique_action(ISSUE_CROSS_REFERENCED, author)
end
- def track_issue_moved_action(author:)
+ def track_issue_moved_action(author:, project:)
+ track_snowplow_action(ISSUE_MOVED, author, project)
track_unique_action(ISSUE_MOVED, author)
end
- def track_issue_related_action(author:)
+ def track_issue_related_action(author:, project:)
+ track_snowplow_action(ISSUE_RELATED, author, project)
track_unique_action(ISSUE_RELATED, author)
end
- def track_issue_unrelated_action(author:)
+ def track_issue_unrelated_action(author:, project:)
+ track_snowplow_action(ISSUE_UNRELATED, author, project)
track_unique_action(ISSUE_UNRELATED, author)
end
- def track_issue_marked_as_duplicate_action(author:)
+ def track_issue_marked_as_duplicate_action(author:, project:)
+ track_snowplow_action(ISSUE_MARKED_AS_DUPLICATE, author, project)
track_unique_action(ISSUE_MARKED_AS_DUPLICATE, author)
end
- def track_issue_locked_action(author:)
+ def track_issue_locked_action(author:, project:)
+ track_snowplow_action(ISSUE_LOCKED, author, project)
track_unique_action(ISSUE_LOCKED, author)
end
- def track_issue_unlocked_action(author:)
+ def track_issue_unlocked_action(author:, project:)
+ track_snowplow_action(ISSUE_UNLOCKED, author, project)
track_unique_action(ISSUE_UNLOCKED, author)
end
- def track_issue_designs_added_action(author:)
+ def track_issue_designs_added_action(author:, project:)
+ track_snowplow_action(ISSUE_DESIGNS_ADDED, author, project)
track_unique_action(ISSUE_DESIGNS_ADDED, author)
end
- def track_issue_designs_modified_action(author:)
+ def track_issue_designs_modified_action(author:, project:)
+ track_snowplow_action(ISSUE_DESIGNS_MODIFIED, author, project)
track_unique_action(ISSUE_DESIGNS_MODIFIED, author)
end
- def track_issue_designs_removed_action(author:)
+ def track_issue_designs_removed_action(author:, project:)
+ track_snowplow_action(ISSUE_DESIGNS_REMOVED, author, project)
track_unique_action(ISSUE_DESIGNS_REMOVED, author)
end
- def track_issue_due_date_changed_action(author:)
+ def track_issue_due_date_changed_action(author:, project:)
+ track_snowplow_action(ISSUE_DUE_DATE_CHANGED, author, project)
track_unique_action(ISSUE_DUE_DATE_CHANGED, author)
end
- def track_issue_time_estimate_changed_action(author:)
+ def track_issue_time_estimate_changed_action(author:, project:)
+ track_snowplow_action(ISSUE_TIME_ESTIMATE_CHANGED, author, project)
track_unique_action(ISSUE_TIME_ESTIMATE_CHANGED, author)
end
- def track_issue_time_spent_changed_action(author:)
+ def track_issue_time_spent_changed_action(author:, project:)
+ track_snowplow_action(ISSUE_TIME_SPENT_CHANGED, author, project)
track_unique_action(ISSUE_TIME_SPENT_CHANGED, author)
end
diff --git a/lib/gitlab/usage_data_counters/known_events/ci_templates.yml b/lib/gitlab/usage_data_counters/known_events/ci_templates.yml
index a8f1bab1f20..10e36a75a3a 100644
--- a/lib/gitlab/usage_data_counters/known_events/ci_templates.yml
+++ b/lib/gitlab/usage_data_counters/known_events/ci_templates.yml
@@ -139,6 +139,10 @@
category: ci_templates
redis_slot: ci_templates
aggregation: weekly
+- name: p_ci_templates_security_container_scanning_latest
+ category: ci_templates
+ redis_slot: ci_templates
+ aggregation: weekly
- name: p_ci_templates_security_api_fuzzing
category: ci_templates
redis_slot: ci_templates
@@ -231,6 +235,10 @@
category: ci_templates
redis_slot: ci_templates
aggregation: weekly
+- name: p_ci_templates_katalon
+ category: ci_templates
+ redis_slot: ci_templates
+ aggregation: weekly
- name: p_ci_templates_mono
category: ci_templates
redis_slot: ci_templates
@@ -319,6 +327,10 @@
category: ci_templates
redis_slot: ci_templates
aggregation: weekly
+- name: p_ci_templates_jobs_license_scanning_latest
+ category: ci_templates
+ redis_slot: ci_templates
+ aggregation: weekly
- name: p_ci_templates_jobs_deploy
category: ci_templates
redis_slot: ci_templates
@@ -331,6 +343,10 @@
category: ci_templates
redis_slot: ci_templates
aggregation: weekly
+- name: p_ci_templates_jobs_dependency_scanning_latest
+ category: ci_templates
+ redis_slot: ci_templates
+ aggregation: weekly
- name: p_ci_templates_jobs_test
category: ci_templates
redis_slot: ci_templates
@@ -523,6 +539,10 @@
category: ci_templates
redis_slot: ci_templates
aggregation: weekly
+- name: p_ci_templates_implicit_jobs_license_scanning_latest
+ category: ci_templates
+ redis_slot: ci_templates
+ aggregation: weekly
- name: p_ci_templates_implicit_jobs_deploy
category: ci_templates
redis_slot: ci_templates
@@ -535,6 +555,10 @@
category: ci_templates
redis_slot: ci_templates
aggregation: weekly
+- name: p_ci_templates_implicit_jobs_dependency_scanning_latest
+ category: ci_templates
+ redis_slot: ci_templates
+ aggregation: weekly
- name: p_ci_templates_implicit_jobs_test
category: ci_templates
redis_slot: ci_templates
@@ -635,6 +659,10 @@
category: ci_templates
redis_slot: ci_templates
aggregation: weekly
+- name: p_ci_templates_implicit_security_container_scanning_latest
+ category: ci_templates
+ redis_slot: ci_templates
+ aggregation: weekly
- name: p_ci_templates_implicit_security_api_fuzzing
category: ci_templates
redis_slot: ci_templates
diff --git a/lib/gitlab/usage_data_counters/known_events/code_review_events.yml b/lib/gitlab/usage_data_counters/known_events/code_review_events.yml
index c21b99ba834..0bd809f8aa5 100644
--- a/lib/gitlab/usage_data_counters/known_events/code_review_events.yml
+++ b/lib/gitlab/usage_data_counters/known_events/code_review_events.yml
@@ -1,9 +1,29 @@
---
-- name: i_code_review_mr_diffs
+- name: i_code_review_create_note_in_ipynb_diff
+ redis_slot: code_review
+ category: code_review
+ aggregation: weekly
+- name: i_code_review_create_note_in_ipynb_diff_mr
+ redis_slot: code_review
+ category: code_review
+ aggregation: weekly
+- name: i_code_review_create_note_in_ipynb_diff_commit
+ redis_slot: code_review
+ category: code_review
+ aggregation: weekly
+- name: i_code_review_user_create_note_in_ipynb_diff
+ redis_slot: code_review
+ category: code_review
+ aggregation: weekly
+- name: i_code_review_user_create_note_in_ipynb_diff_mr
+ redis_slot: code_review
+ category: code_review
+ aggregation: weekly
+- name: i_code_review_user_create_note_in_ipynb_diff_commit
redis_slot: code_review
category: code_review
aggregation: weekly
-- name: i_code_review_mr_with_invalid_approvers
+- name: i_code_review_mr_diffs
redis_slot: code_review
category: code_review
aggregation: weekly
@@ -135,12 +155,10 @@
redis_slot: code_review
category: code_review
aggregation: weekly
- feature_flag: usage_data_i_code_review_user_jetbrains_api_request
- name: i_code_review_user_gitlab_cli_api_request
redis_slot: code_review
category: code_review
aggregation: weekly
- feature_flag: usage_data_i_code_review_user_gitlab_cli_api_request
- name: i_code_review_user_create_mr_from_issue
redis_slot: code_review
category: code_review
@@ -177,30 +195,6 @@
redis_slot: code_review
category: code_review
aggregation: weekly
-- name: i_code_review_create_note_in_ipynb_diff
- redis_slot: code_review
- category: code_review
- aggregation: weekly
-- name: i_code_review_user_create_note_in_ipynb_diff
- redis_slot: code_review
- category: code_review
- aggregation: weekly
-- name: i_code_review_create_note_in_ipynb_diff_mr
- redis_slot: code_review
- category: code_review
- aggregation: weekly
-- name: i_code_review_user_create_note_in_ipynb_diff_mr
- redis_slot: code_review
- category: code_review
- aggregation: weekly
-- name: i_code_review_create_note_in_ipynb_diff_commit
- redis_slot: code_review
- category: code_review
- aggregation: weekly
-- name: i_code_review_user_create_note_in_ipynb_diff_commit
- redis_slot: code_review
- category: code_review
- aggregation: weekly
# Diff settings events
- name: i_code_review_click_diff_view_setting
redis_slot: code_review
@@ -400,53 +394,36 @@
redis_slot: code_review
category: code_review
aggregation: weekly
-## Metrics
-- name: i_code_review_merge_request_widget_metrics_view
- redis_slot: code_review
- category: code_review
- aggregation: weekly
-- name: i_code_review_merge_request_widget_metrics_full_report_clicked
- redis_slot: code_review
- category: code_review
- aggregation: weekly
-- name: i_code_review_merge_request_widget_metrics_expand
- redis_slot: code_review
- category: code_review
- aggregation: weekly
-- name: i_code_review_merge_request_widget_metrics_expand_success
- redis_slot: code_review
- category: code_review
- aggregation: weekly
-- name: i_code_review_merge_request_widget_metrics_expand_warning
+- name: i_code_review_submit_review_approve
redis_slot: code_review
category: code_review
aggregation: weekly
-- name: i_code_review_merge_request_widget_metrics_expand_failed
+- name: i_code_review_submit_review_comment
redis_slot: code_review
category: code_review
aggregation: weekly
-## Status Checks
-- name: i_code_review_merge_request_widget_status_checks_view
+## License Compliance
+- name: i_code_review_merge_request_widget_license_compliance_view
redis_slot: code_review
category: code_review
aggregation: weekly
-- name: i_code_review_merge_request_widget_status_checks_full_report_clicked
+- name: i_code_review_merge_request_widget_license_compliance_full_report_clicked
redis_slot: code_review
category: code_review
aggregation: weekly
-- name: i_code_review_merge_request_widget_status_checks_expand
+- name: i_code_review_merge_request_widget_license_compliance_expand
redis_slot: code_review
category: code_review
aggregation: weekly
-- name: i_code_review_merge_request_widget_status_checks_expand_success
+- name: i_code_review_merge_request_widget_license_compliance_expand_success
redis_slot: code_review
category: code_review
aggregation: weekly
-- name: i_code_review_merge_request_widget_status_checks_expand_warning
+- name: i_code_review_merge_request_widget_license_compliance_expand_warning
redis_slot: code_review
category: code_review
aggregation: weekly
-- name: i_code_review_merge_request_widget_status_checks_expand_failed
+- name: i_code_review_merge_request_widget_license_compliance_expand_failed
redis_slot: code_review
category: code_review
aggregation: weekly
diff --git a/lib/gitlab/usage_data_counters/known_events/common.yml b/lib/gitlab/usage_data_counters/known_events/common.yml
index 6c4754ae19f..29b231f88f8 100644
--- a/lib/gitlab/usage_data_counters/known_events/common.yml
+++ b/lib/gitlab/usage_data_counters/known_events/common.yml
@@ -146,6 +146,11 @@
category: testing
redis_slot: testing
aggregation: weekly
+- name: i_testing_test_report_uploaded
+ category: testing
+ redis_slot: testing
+ aggregation: weekly
+ feature_flag: usage_data_ci_i_testing_test_report_uploaded
# Project Management group
- name: g_project_management_issue_title_changed
category: issues_edit
@@ -332,11 +337,6 @@
redis_slot: testing
category: testing
aggregation: weekly
-# Container Security - Network Policies
-- name: clusters_using_network_policies_ui
- redis_slot: network_policies
- category: network_policies
- aggregation: weekly
# Geo group
- name: g_geo_proxied_requests
category: geo
@@ -352,3 +352,8 @@
category: manage
aggregation: weekly
expiry: 42
+# Environments page
+- name: users_visiting_environments_pages
+ category: environments
+ redis_slot: users
+ aggregation: weekly
diff --git a/lib/gitlab/usage_data_counters/known_events/ecosystem.yml b/lib/gitlab/usage_data_counters/known_events/ecosystem.yml
index f594c6a1b7c..7f7c9166086 100644
--- a/lib/gitlab/usage_data_counters/known_events/ecosystem.yml
+++ b/lib/gitlab/usage_data_counters/known_events/ecosystem.yml
@@ -8,14 +8,6 @@
category: ecosystem
redis_slot: ecosystem
aggregation: weekly
-- name: i_ecosystem_jira_service_list_issues
- category: ecosystem
- redis_slot: ecosystem
- aggregation: weekly
-- name: i_ecosystem_jira_service_create_issue
- category: ecosystem
- redis_slot: ecosystem
- aggregation: weekly
- name: i_ecosystem_slack_service_issue_notification
category: ecosystem
redis_slot: ecosystem
diff --git a/lib/gitlab/usage_data_counters/known_events/epic_board_events.yml b/lib/gitlab/usage_data_counters/known_events/epic_board_events.yml
deleted file mode 100644
index 3879c561cc4..00000000000
--- a/lib/gitlab/usage_data_counters/known_events/epic_board_events.yml
+++ /dev/null
@@ -1,19 +0,0 @@
-# Epic board events
-#
-# We are using the same slot of issue events 'project_management' for
-# epic events to allow data aggregation.
-# More information in: https://gitlab.com/gitlab-org/gitlab/-/issues/322405
-- name: g_project_management_users_creating_epic_boards
- category: epic_boards_usage
- redis_slot: project_management
- aggregation: daily
-
-- name: g_project_management_users_viewing_epic_boards
- category: epic_boards_usage
- redis_slot: project_management
- aggregation: daily
-
-- name: g_project_management_users_updating_epic_board_names
- category: epic_boards_usage
- redis_slot: project_management
- aggregation: daily
diff --git a/lib/gitlab/usage_data_counters/known_events/kubernetes_agent.yml b/lib/gitlab/usage_data_counters/known_events/kubernetes_agent.yml
index e1de74a3d07..966e6c584c7 100644
--- a/lib/gitlab/usage_data_counters/known_events/kubernetes_agent.yml
+++ b/lib/gitlab/usage_data_counters/known_events/kubernetes_agent.yml
@@ -2,4 +2,3 @@
category: kubernetes_agent
redis_slot: agent
aggregation: weekly
- feature_flag: track_agent_users_using_ci_tunnel
diff --git a/lib/gitlab/usage_data_counters/known_events/quickactions.yml b/lib/gitlab/usage_data_counters/known_events/quickactions.yml
index f980503b4bf..58a0c0695af 100644
--- a/lib/gitlab/usage_data_counters/known_events/quickactions.yml
+++ b/lib/gitlab/usage_data_counters/known_events/quickactions.yml
@@ -127,6 +127,10 @@
category: quickactions
redis_slot: quickactions
aggregation: weekly
+- name: i_quickactions_timeline
+ category: quickactions
+ redis_slot: quickactions
+ aggregation: weekly
- name: i_quickactions_page
category: quickactions
redis_slot: quickactions
@@ -303,11 +307,3 @@
category: quickactions
redis_slot: quickactions
aggregation: weekly
-- name: i_quickactions_attention
- category: quickactions
- redis_slot: quickactions
- aggregation: weekly
-- name: i_quickactions_remove_attention
- category: quickactions
- redis_slot: quickactions
- aggregation: weekly
diff --git a/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter.rb b/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter.rb
index fbb03a31a6f..93137b762ec 100644
--- a/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter.rb
+++ b/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter.rb
@@ -49,6 +49,8 @@ module Gitlab
MR_LOAD_CONFLICT_UI_ACTION = 'i_code_review_user_load_conflict_ui'
MR_RESOLVE_CONFLICT_ACTION = 'i_code_review_user_resolve_conflict'
MR_RESOLVE_THREAD_IN_ISSUE_ACTION = 'i_code_review_user_resolve_thread_in_issue'
+ MR_SUBMIT_REVIEW_APPROVE = 'i_code_review_submit_review_approve'
+ MR_SUBMIT_REVIEW_COMMENT = 'i_code_review_submit_review_comment'
class << self
def track_mr_diffs_action(merge_request:)
@@ -230,6 +232,14 @@ module Gitlab
track_unique_action_by_user(MR_RESOLVE_THREAD_IN_ISSUE_ACTION, user)
end
+ def track_submit_review_approve(user:)
+ track_unique_action_by_user(MR_SUBMIT_REVIEW_APPROVE, user)
+ end
+
+ def track_submit_review_comment(user:)
+ track_unique_action_by_user(MR_SUBMIT_REVIEW_COMMENT, user)
+ end
+
private
def track_unique_action_by_merge_request(action, merge_request)
diff --git a/lib/gitlab/usage_data_counters/merge_request_widget_extension_counter.rb b/lib/gitlab/usage_data_counters/merge_request_widget_extension_counter.rb
index dafc36ab7ce..f88bbc41c70 100644
--- a/lib/gitlab/usage_data_counters/merge_request_widget_extension_counter.rb
+++ b/lib/gitlab/usage_data_counters/merge_request_widget_extension_counter.rb
@@ -5,7 +5,7 @@ module Gitlab
class MergeRequestWidgetExtensionCounter < BaseCounter
KNOWN_EVENTS = %w[view full_report_clicked expand expand_success expand_warning expand_failed].freeze
PREFIX = 'i_code_review_merge_request_widget'
- WIDGETS = %w[accessibility code_quality status_checks terraform test_summary metrics].freeze
+ WIDGETS = %w[accessibility code_quality license_compliance status_checks terraform test_summary metrics].freeze
class << self
private
diff --git a/lib/gitlab/utils/deep_size.rb b/lib/gitlab/utils/deep_size.rb
index e185786e638..20f2d699e2b 100644
--- a/lib/gitlab/utils/deep_size.rb
+++ b/lib/gitlab/utils/deep_size.rb
@@ -25,10 +25,6 @@ module Gitlab
!too_big? && !too_deep?
end
- def self.human_default_max_size
- ActiveSupport::NumberHelper.number_to_human_size(DEFAULT_MAX_SIZE)
- end
-
private
def evaluate
diff --git a/lib/gitlab/utils/execution_tracker.rb b/lib/gitlab/utils/execution_tracker.rb
new file mode 100644
index 00000000000..6d48658853c
--- /dev/null
+++ b/lib/gitlab/utils/execution_tracker.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Utils
+ class ExecutionTracker
+ MAX_RUNTIME = 30.seconds
+
+ ExecutionTimeOutError = Class.new(StandardError)
+
+ delegate :monotonic_time, to: :'Gitlab::Metrics::System'
+
+ def initialize
+ @start_time = monotonic_time
+ end
+
+ def over_limit?
+ monotonic_time - start_time >= MAX_RUNTIME
+ end
+
+ private
+
+ attr_reader :start_time
+ end
+ end
+end
diff --git a/lib/gitlab/view/presenter/base.rb b/lib/gitlab/view/presenter/base.rb
index a2d217fb42f..2a57ca9ae02 100644
--- a/lib/gitlab/view/presenter/base.rb
+++ b/lib/gitlab/view/presenter/base.rb
@@ -46,6 +46,13 @@ module Gitlab
url_builder.build(__subject__, only_path: true)
end
+ def path_with_line_numbers(path, start_line, end_line)
+ path.tap do |complete_path|
+ complete_path << "#L#{start_line}"
+ complete_path << "-#{end_line}" if end_line && end_line != start_line
+ end
+ end
+
class_methods do
def presenter?
true
diff --git a/lib/gitlab/visibility_level.rb b/lib/gitlab/visibility_level.rb
index 049e3befe64..7360585df43 100644
--- a/lib/gitlab/visibility_level.rb
+++ b/lib/gitlab/visibility_level.rb
@@ -47,17 +47,17 @@ module Gitlab
def options
{
- s_('VisibilityLevel|Private') => PRIVATE,
+ s_('VisibilityLevel|Private') => PRIVATE,
s_('VisibilityLevel|Internal') => INTERNAL,
- s_('VisibilityLevel|Public') => PUBLIC
+ s_('VisibilityLevel|Public') => PUBLIC
}
end
def string_options
{
- 'private' => PRIVATE,
+ 'private' => PRIVATE,
'internal' => INTERNAL,
- 'public' => PUBLIC
+ 'public' => PUBLIC
}
end
diff --git a/lib/gitlab/web_hooks/recursion_detection.rb b/lib/gitlab/web_hooks/recursion_detection.rb
index 1b5350d4a4e..031d9ec6ec4 100644
--- a/lib/gitlab/web_hooks/recursion_detection.rb
+++ b/lib/gitlab/web_hooks/recursion_detection.rb
@@ -40,9 +40,9 @@ module Gitlab
cache_key = cache_key_for_hook(hook)
::Gitlab::Redis::SharedState.with do |redis|
- redis.multi do
- redis.sadd(cache_key, hook.id)
- redis.expire(cache_key, TOUCH_CACHE_TTL)
+ redis.multi do |multi|
+ multi.sadd(cache_key, hook.id)
+ multi.expire(cache_key, TOUCH_CACHE_TTL)
end
end
end
diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb
index e81670ce89a..906439d5e71 100644
--- a/lib/gitlab/workhorse.rb
+++ b/lib/gitlab/workhorse.rb
@@ -12,7 +12,7 @@ module Gitlab
VERSION_FILE = 'GITLAB_WORKHORSE_VERSION'
INTERNAL_API_CONTENT_TYPE = 'application/vnd.gitlab-workhorse+json'
INTERNAL_API_REQUEST_HEADER = 'Gitlab-Workhorse-Api-Request'
- NOTIFICATION_CHANNEL = 'workhorse:notifications'
+ NOTIFICATION_PREFIX = 'workhorse:notifications:'
ALLOWED_GIT_HTTP_ACTIONS = %w[git_receive_pack git_upload_pack info_refs].freeze
DETECT_HEADER = 'Gitlab-Workhorse-Detect-Content-Type'
ARCHIVE_FORMATS = %w(zip tar.gz tar.bz2 tar).freeze
@@ -217,7 +217,8 @@ module Gitlab
Gitlab::Redis::SharedState.with do |redis|
result = redis.set(key, value, ex: expire, nx: !overwrite)
if result
- redis.publish(NOTIFICATION_CHANNEL, "#{key}=#{value}")
+ redis.publish(NOTIFICATION_PREFIX + key, value)
+
value
else
redis.get(key)
diff --git a/lib/gitlab_edition.rb b/lib/gitlab_edition.rb
index 02006148a34..5e3ed35ace4 100644
--- a/lib/gitlab_edition.rb
+++ b/lib/gitlab_edition.rb
@@ -7,6 +7,21 @@ module GitlabEdition
Pathname.new(File.expand_path('..', __dir__))
end
+ def self.path_glob(path)
+ "#{root}/#{extension_path_prefixes}#{path}"
+ end
+
+ def self.extension_path_prefixes
+ path_prefixes = extensions
+ return '' if path_prefixes.empty?
+
+ path_prefixes.map! { "#{_1}/" }
+ path_prefixes.unshift ''
+
+ # For example `{,ee/,jh/}`
+ "{#{path_prefixes.join(',')}}"
+ end
+
def self.extensions
if jh?
%w[ee jh]
diff --git a/lib/google_api/cloud_platform/client.rb b/lib/google_api/cloud_platform/client.rb
index 39cf994ca3f..38a1a968aec 100644
--- a/lib/google_api/cloud_platform/client.rb
+++ b/lib/google_api/cloud_platform/client.rb
@@ -22,7 +22,7 @@ module GoogleApi
"https://www.googleapis.com/auth/logging.write",
"https://www.googleapis.com/auth/monitoring"
].freeze
- ROLES_LIST = %w[roles/iam.serviceAccountUser roles/artifactregistry.admin roles/cloudbuild.builds.builder roles/run.admin roles/storage.admin roles/cloudsql.admin roles/browser].freeze
+ ROLES_LIST = %w[roles/iam.serviceAccountUser roles/artifactregistry.admin roles/cloudbuild.builds.builder roles/run.admin roles/storage.admin roles/cloudsql.client roles/browser].freeze
REVOKE_URL = 'https://oauth2.googleapis.com/revoke'
class << self
diff --git a/lib/learn_gitlab/onboarding.rb b/lib/learn_gitlab/onboarding.rb
deleted file mode 100644
index 54af01a21fe..00000000000
--- a/lib/learn_gitlab/onboarding.rb
+++ /dev/null
@@ -1,69 +0,0 @@
-# frozen_string_literal: true
-
-module LearnGitlab
- class Onboarding
- include Gitlab::Utils::StrongMemoize
- include Gitlab::Experiment::Dsl
-
- ACTION_ISSUE_IDS = {
- pipeline_created: 7,
- trial_started: 2,
- required_mr_approvals_enabled: 11,
- code_owners_enabled: 10
- }.freeze
-
- ACTION_PATHS = [
- :issue_created,
- :git_write,
- :merge_request_created,
- :user_added
- ].freeze
-
- def initialize(namespace, current_user = nil)
- @namespace = namespace
- @current_user = current_user
- end
-
- def completed_percentage
- return 0 unless onboarding_progress
-
- attributes = onboarding_progress.attributes.symbolize_keys
-
- total_actions = action_columns.count
- completed_actions = action_columns.count { |column| attributes[column].present? }
-
- (completed_actions.to_f / total_actions.to_f * 100).round
- end
-
- private
-
- def onboarding_progress
- strong_memoize(:onboarding_progress) do
- OnboardingProgress.find_by(namespace: namespace) # rubocop: disable CodeReuse/ActiveRecord
- end
- end
-
- def action_columns
- strong_memoize(:action_columns) do
- tracked_actions.map { |action_key| OnboardingProgress.column_name(action_key) }
- end
- end
-
- def tracked_actions
- ACTION_ISSUE_IDS.keys + ACTION_PATHS + deploy_section_tracked_actions
- end
-
- def deploy_section_tracked_actions
- experiment(:security_actions_continuous_onboarding,
- namespace: namespace,
- user: current_user,
- sticky_to: current_user
- ) do |e|
- e.control { [:security_scan_enabled] }
- e.candidate { [:license_scanning_run, :secure_dependency_scanning_run, :secure_dast_run] }
- end.run
- end
-
- attr_reader :namespace, :current_user
- end
-end
diff --git a/lib/learn_gitlab/project.rb b/lib/learn_gitlab/project.rb
deleted file mode 100644
index 64f91dcf1a8..00000000000
--- a/lib/learn_gitlab/project.rb
+++ /dev/null
@@ -1,38 +0,0 @@
-# frozen_string_literal: true
-
-module LearnGitlab
- class Project
- PROJECT_NAME = 'Learn GitLab'
- PROJECT_NAME_ULTIMATE_TRIAL = 'Learn GitLab - Ultimate trial'
- BOARD_NAME = 'GitLab onboarding'
- LABEL_NAME = 'Novice'
-
- def initialize(current_user)
- @current_user = current_user
- end
-
- def available?
- project && board && label
- end
-
- def project
- @project ||= current_user.projects.find_by_name([PROJECT_NAME, PROJECT_NAME_ULTIMATE_TRIAL])
- end
-
- def board
- return unless project
-
- @board ||= project.boards.find_by_name(BOARD_NAME)
- end
-
- def label
- return unless project
-
- @label ||= project.labels.find_by_name(LABEL_NAME)
- end
-
- private
-
- attr_reader :current_user
- end
-end
diff --git a/lib/object_storage/direct_upload.rb b/lib/object_storage/direct_upload.rb
index a8b51a95e59..d092cd56e46 100644
--- a/lib/object_storage/direct_upload.rb
+++ b/lib/object_storage/direct_upload.rb
@@ -206,7 +206,7 @@ module ObjectStorage
def requires_multipart_upload?
return false unless config.aws?
- return false if use_workhorse_s3_client? && Feature.enabled?(:s3_omit_multipart_urls)
+ return false if use_workhorse_s3_client?
!has_length
end
diff --git a/lib/omni_auth/strategies/bitbucket.rb b/lib/omni_auth/strategies/bitbucket.rb
index 6c914b4222a..d64f3dd987d 100644
--- a/lib/omni_auth/strategies/bitbucket.rb
+++ b/lib/omni_auth/strategies/bitbucket.rb
@@ -40,7 +40,7 @@ module OmniAuth
end
def callback_url
- options[:redirect_uri] || (full_host + script_name + callback_path)
+ options[:redirect_uri] || (full_host + callback_path)
end
end
end
diff --git a/lib/peek/views/redis_detailed.rb b/lib/peek/views/redis_detailed.rb
index 44ec0ec0f68..76c283bf802 100644
--- a/lib/peek/views/redis_detailed.rb
+++ b/lib/peek/views/redis_detailed.rb
@@ -16,7 +16,11 @@ module Peek
private
def format_call_details(call)
- super.merge(cmd: format_command(call[:cmd]),
+ cmd = call[:commands].map do |command|
+ format_command(command)
+ end.join(', ')
+
+ super.merge(cmd: cmd,
instance: call[:storage])
end
diff --git a/lib/product_analytics/event_params.rb b/lib/product_analytics/event_params.rb
index 07e0bc8b43a..6cb3d462384 100644
--- a/lib/product_analytics/event_params.rb
+++ b/lib/product_analytics/event_params.rb
@@ -11,41 +11,41 @@ module ProductAnalytics
class EventParams
def self.parse_event_params(params)
{
- project_id: params['aid'],
- platform: params['p'],
- collector_tstamp: Time.zone.now,
- event_id: params['eid'],
- v_tracker: params['tv'],
- v_collector: Gitlab::VERSION,
- v_etl: Gitlab::VERSION,
- os_timezone: params['tz'],
- name_tracker: params['tna'],
- br_lang: params['lang'],
- doc_charset: params['cs'],
- br_features_pdf: Gitlab::Utils.to_boolean(params['f_pdf']),
- br_features_flash: Gitlab::Utils.to_boolean(params['f_fla']),
- br_features_java: Gitlab::Utils.to_boolean(params['f_java']),
- br_features_director: Gitlab::Utils.to_boolean(params['f_dir']),
- br_features_quicktime: Gitlab::Utils.to_boolean(params['f_qt']),
- br_features_realplayer: Gitlab::Utils.to_boolean(params['f_realp']),
+ project_id: params['aid'],
+ platform: params['p'],
+ collector_tstamp: Time.zone.now,
+ event_id: params['eid'],
+ v_tracker: params['tv'],
+ v_collector: Gitlab::VERSION,
+ v_etl: Gitlab::VERSION,
+ os_timezone: params['tz'],
+ name_tracker: params['tna'],
+ br_lang: params['lang'],
+ doc_charset: params['cs'],
+ br_features_pdf: Gitlab::Utils.to_boolean(params['f_pdf']),
+ br_features_flash: Gitlab::Utils.to_boolean(params['f_fla']),
+ br_features_java: Gitlab::Utils.to_boolean(params['f_java']),
+ br_features_director: Gitlab::Utils.to_boolean(params['f_dir']),
+ br_features_quicktime: Gitlab::Utils.to_boolean(params['f_qt']),
+ br_features_realplayer: Gitlab::Utils.to_boolean(params['f_realp']),
br_features_windowsmedia: Gitlab::Utils.to_boolean(params['f_wma']),
- br_features_gears: Gitlab::Utils.to_boolean(params['f_gears']),
- br_features_silverlight: Gitlab::Utils.to_boolean(params['f_ag']),
- br_colordepth: params['cd'],
- br_cookies: Gitlab::Utils.to_boolean(params['cookie']),
- dvce_created_tstamp: params['dtm'],
- br_viewheight: params['vp'],
- domain_sessionidx: params['vid'],
- domain_sessionid: params['sid'],
- domain_userid: params['duid'],
- user_fingerprint: params['fp'],
- page_referrer: params['refr'],
- page_url: params['url'],
- se_category: params['se_ca'],
- se_action: params['se_ac'],
- se_label: params['se_la'],
- se_property: params['se_pr'],
- se_value: params['se_va']
+ br_features_gears: Gitlab::Utils.to_boolean(params['f_gears']),
+ br_features_silverlight: Gitlab::Utils.to_boolean(params['f_ag']),
+ br_colordepth: params['cd'],
+ br_cookies: Gitlab::Utils.to_boolean(params['cookie']),
+ dvce_created_tstamp: params['dtm'],
+ br_viewheight: params['vp'],
+ domain_sessionidx: params['vid'],
+ domain_sessionid: params['sid'],
+ domain_userid: params['duid'],
+ user_fingerprint: params['fp'],
+ page_referrer: params['refr'],
+ page_url: params['url'],
+ se_category: params['se_ca'],
+ se_action: params['se_ac'],
+ se_label: params['se_la'],
+ se_property: params['se_pr'],
+ se_value: params['se_va']
}
end
diff --git a/lib/security/weak_passwords.rb b/lib/security/weak_passwords.rb
new file mode 100644
index 00000000000..42b02132933
--- /dev/null
+++ b/lib/security/weak_passwords.rb
@@ -0,0 +1,88 @@
+# frozen_string_literal: true
+module Security
+ module WeakPasswords
+ # These words are predictable in GitLab's specific context, and
+ # therefore cannot occur anywhere within a password.
+ FORBIDDEN_WORDS = Set['gitlab', 'devops'].freeze
+
+ # Substrings shorter than this may appear legitimately in a truly
+ # random password.
+ MINIMUM_SUBSTRING_SIZE = 4
+
+ class << self
+ # Returns true when the password is on a list of weak passwords,
+ # or contains predictable substrings derived from user attributes.
+ # Case insensitive.
+ def weak_for_user?(password, user)
+ forbidden_word_appears_in_password?(password) ||
+ name_appears_in_password?(password, user) ||
+ username_appears_in_password?(password, user) ||
+ email_appears_in_password?(password, user) ||
+ password_on_weak_list?(password)
+ end
+
+ private
+
+ def forbidden_word_appears_in_password?(password)
+ contains_predicatable_substring?(password, FORBIDDEN_WORDS)
+ end
+
+ def name_appears_in_password?(password, user)
+ return false if user.name.blank?
+
+ # Check for the full name
+ substrings = [user.name]
+ # Also check parts of their name
+ substrings += user.name.split(/[^\p{Alnum}]/)
+
+ contains_predicatable_substring?(password, substrings)
+ end
+
+ def username_appears_in_password?(password, user)
+ return false if user.username.blank?
+
+ # Check for the full username
+ substrings = [user.username]
+ # Also check sub-strings in the username
+ substrings += user.username.split(/[^\p{Alnum}]/)
+
+ contains_predicatable_substring?(password, substrings)
+ end
+
+ def email_appears_in_password?(password, user)
+ return false if user.email.blank?
+
+ # Check for the full email
+ substrings = [user.email]
+ # Also check full first part and full domain name
+ substrings += user.email.split("@")
+ # And any parts of non-word characters (e.g. firstname.lastname+tag@...)
+ substrings += user.email.split(/[^\p{Alnum}]/)
+
+ contains_predicatable_substring?(password, substrings)
+ end
+
+ def password_on_weak_list?(password)
+ # Our weak list stores SHA2 hashes of passwords, not the weak
+ # passwords themselves.
+ digest = Digest::SHA256.base64digest(password.downcase)
+ Settings.gitlab.weak_passwords_digest_set.include?(digest)
+ end
+
+ # Case-insensitively checks whether a password includes a dynamic
+ # list of substrings. Substrings which are too short are not
+ # predictable and may occur randomly, and therefore not checked.
+ def contains_predicatable_substring?(password, substrings)
+ substrings = substrings.filter_map do |substring|
+ substring.downcase if substring.length >= MINIMUM_SUBSTRING_SIZE
+ end
+
+ password = password.downcase
+
+ # Returns true when a predictable substring occurs anywhere
+ # in the password.
+ substrings.any? { |word| password.include?(word) }
+ end
+ end
+ end
+end
diff --git a/lib/sidebars/groups/menus/observability_menu.rb b/lib/sidebars/groups/menus/observability_menu.rb
new file mode 100644
index 00000000000..b479ff3c492
--- /dev/null
+++ b/lib/sidebars/groups/menus/observability_menu.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module Sidebars
+ module Groups
+ module Menus
+ class ObservabilityMenu < ::Sidebars::Menu
+ override :link
+ def link
+ group_observability_index_path(context.group)
+ end
+
+ override :title
+ def title
+ _('Observability')
+ end
+
+ override :sprite_icon
+ def sprite_icon
+ 'monitor'
+ end
+
+ override :render?
+ def render?
+ can?(context.current_user, :read_observability, context.group)
+ end
+
+ override :active_routes
+ def active_routes
+ { controller: :observability, path: 'groups#observability' }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/sidebars/groups/menus/packages_registries_menu.rb b/lib/sidebars/groups/menus/packages_registries_menu.rb
index fda90406e0a..61cd81711f8 100644
--- a/lib/sidebars/groups/menus/packages_registries_menu.rb
+++ b/lib/sidebars/groups/menus/packages_registries_menu.rb
@@ -15,7 +15,7 @@ module Sidebars
override :title
def title
- _('Packages & Registries')
+ _('Packages and registries')
end
override :sprite_icon
@@ -50,7 +50,9 @@ module Sidebars
end
def harbor_registry__menu_item
- return nil_menu_item(:harbor_registry) if Feature.disabled?(:harbor_registry_integration)
+ if Feature.disabled?(:harbor_registry_integration) || context.group.harbor_integration.nil?
+ return nil_menu_item(:harbor_registry)
+ end
::Sidebars::MenuItem.new(
title: _('Harbor Registry'),
diff --git a/lib/sidebars/groups/menus/settings_menu.rb b/lib/sidebars/groups/menus/settings_menu.rb
index 18ff3ebc714..df170670aab 100644
--- a/lib/sidebars/groups/menus/settings_menu.rb
+++ b/lib/sidebars/groups/menus/settings_menu.rb
@@ -6,18 +6,23 @@ module Sidebars
class SettingsMenu < ::Sidebars::Menu
override :configure_menu_items
def configure_menu_items
- return false unless can?(context.current_user, :admin_group, context.group)
-
- add_item(general_menu_item)
- add_item(integrations_menu_item)
- add_item(access_tokens_menu_item)
- add_item(group_projects_menu_item)
- add_item(repository_menu_item)
- add_item(ci_cd_menu_item)
- add_item(applications_menu_item)
- add_item(packages_and_registries_menu_item)
-
- true
+ if can?(context.current_user, :admin_group, context.group)
+ add_item(general_menu_item)
+ add_item(integrations_menu_item)
+ add_item(access_tokens_menu_item)
+ add_item(group_projects_menu_item)
+ add_item(repository_menu_item)
+ add_item(ci_cd_menu_item)
+ add_item(applications_menu_item)
+ add_item(packages_and_registries_menu_item)
+ return true
+ elsif Gitlab.ee? && can?(context.current_user, :change_push_rules, context.group)
+ # Push Rules are the only group setting that can also be edited by maintainers.
+ # Create an empty sub-menu here and EE adds Repository menu item (with only Push Rules).
+ return true
+ end
+
+ false
end
override :title
@@ -112,7 +117,7 @@ module Sidebars
end
::Sidebars::MenuItem.new(
- title: _('Packages & Registries'),
+ title: _('Packages and registries'),
link: group_settings_packages_and_registries_path(context.group),
active_routes: { controller: :packages_and_registries },
item_id: :packages_and_registries
diff --git a/lib/sidebars/groups/panel.rb b/lib/sidebars/groups/panel.rb
index 463c2571b14..e8b815bdce7 100644
--- a/lib/sidebars/groups/panel.rb
+++ b/lib/sidebars/groups/panel.rb
@@ -12,6 +12,7 @@ module Sidebars
add_menu(Sidebars::Groups::Menus::MergeRequestsMenu.new(context))
add_menu(Sidebars::Groups::Menus::CiCdMenu.new(context))
add_menu(Sidebars::Groups::Menus::KubernetesMenu.new(context))
+ add_menu(Sidebars::Groups::Menus::ObservabilityMenu.new(context))
add_menu(Sidebars::Groups::Menus::PackagesRegistriesMenu.new(context))
add_menu(Sidebars::Groups::Menus::CustomerRelationsMenu.new(context))
add_menu(Sidebars::Groups::Menus::SettingsMenu.new(context))
diff --git a/lib/sidebars/projects/menus/infrastructure_menu.rb b/lib/sidebars/projects/menus/infrastructure_menu.rb
index 1c04a7b117d..63eea0ea500 100644
--- a/lib/sidebars/projects/menus/infrastructure_menu.rb
+++ b/lib/sidebars/projects/menus/infrastructure_menu.rb
@@ -54,12 +54,12 @@ module Sidebars
{ disabled: true,
data: { trigger: 'manual',
- container: 'body',
- placement: 'right',
- highlight: Users::CalloutsHelper::GKE_CLUSTER_INTEGRATION,
- highlight_priority: Users::Callout.feature_names[:GKE_CLUSTER_INTEGRATION],
- dismiss_endpoint: callouts_path,
- auto_devops_help_path: help_page_path('topics/autodevops/index.md') } }
+ container: 'body',
+ placement: 'right',
+ highlight: Users::CalloutsHelper::GKE_CLUSTER_INTEGRATION,
+ highlight_priority: Users::Callout.feature_names[:GKE_CLUSTER_INTEGRATION],
+ dismiss_endpoint: callouts_path,
+ auto_devops_help_path: help_page_path('topics/autodevops/index.md') } }
end
def terraform_menu_item
diff --git a/lib/sidebars/projects/menus/learn_gitlab_menu.rb b/lib/sidebars/projects/menus/learn_gitlab_menu.rb
index d2bc2fa0681..b6fae2af93d 100644
--- a/lib/sidebars/projects/menus/learn_gitlab_menu.rb
+++ b/lib/sidebars/projects/menus/learn_gitlab_menu.rb
@@ -29,10 +29,10 @@ module Sidebars
override :pill_count
def pill_count
strong_memoize(:pill_count) do
- percentage = LearnGitlab::Onboarding.new(
+ percentage = Onboarding::Completion.new(
context.project.namespace,
context.current_user
- ).completed_percentage
+ ).percentage
"#{percentage}%"
end
diff --git a/lib/sidebars/projects/menus/merge_requests_menu.rb b/lib/sidebars/projects/menus/merge_requests_menu.rb
index fe501667d37..3e543872d36 100644
--- a/lib/sidebars/projects/menus/merge_requests_menu.rb
+++ b/lib/sidebars/projects/menus/merge_requests_menu.rb
@@ -59,9 +59,9 @@ module Sidebars
override :active_routes
def active_routes
if context.project.issues_enabled?
- { controller: :merge_requests }
+ { controller: 'projects/merge_requests' }
else
- { controller: [:merge_requests, :milestones] }
+ { controller: ['projects/merge_requests', :milestones] }
end
end
end
diff --git a/lib/sidebars/projects/menus/monitor_menu.rb b/lib/sidebars/projects/menus/monitor_menu.rb
index 23e1a95c401..ecd062f333e 100644
--- a/lib/sidebars/projects/menus/monitor_menu.rb
+++ b/lib/sidebars/projects/menus/monitor_menu.rb
@@ -6,7 +6,7 @@ module Sidebars
class MonitorMenu < ::Sidebars::Menu
override :configure_menu_items
def configure_menu_items
- return false unless context.project.feature_available?(:operations, context.current_user)
+ return false unless feature_enabled?
add_item(metrics_dashboard_menu_item)
add_item(error_tracking_menu_item)
@@ -41,6 +41,14 @@ module Sidebars
private
+ def feature_enabled?
+ if ::Feature.enabled?(:split_operations_visibility_permissions, context.project)
+ context.project.feature_available?(:monitor, context.current_user)
+ else
+ context.project.feature_available?(:operations, context.current_user)
+ end
+ end
+
def metrics_dashboard_menu_item
unless can?(context.current_user, :metrics_dashboard, context.project)
return ::Sidebars::NilMenuItem.new(item_id: :metrics)
diff --git a/lib/sidebars/projects/menus/packages_registries_menu.rb b/lib/sidebars/projects/menus/packages_registries_menu.rb
index 914368e6fec..2ddffe42899 100644
--- a/lib/sidebars/projects/menus/packages_registries_menu.rb
+++ b/lib/sidebars/projects/menus/packages_registries_menu.rb
@@ -15,7 +15,7 @@ module Sidebars
override :title
def title
- _('Packages & Registries')
+ _('Packages and registries')
end
override :sprite_icon
@@ -66,7 +66,9 @@ module Sidebars
end
def harbor_registry__menu_item
- return ::Sidebars::NilMenuItem.new(item_id: :harbor_registry) if Feature.disabled?(:harbor_registry_integration)
+ if Feature.disabled?(:harbor_registry_integration, context.project) || context.project.harbor_integration.nil?
+ return ::Sidebars::NilMenuItem.new(item_id: :harbor_registry)
+ end
::Sidebars::MenuItem.new(
title: _('Harbor Registry'),
@@ -77,7 +79,8 @@ module Sidebars
end
def packages_registry_disabled?
- !::Gitlab.config.packages.enabled || !can?(context.current_user, :read_package, context.project)
+ !::Gitlab.config.packages.enabled ||
+ !can?(context.current_user, :read_package, context.project&.packages_policy_subject)
end
end
end
diff --git a/lib/sidebars/projects/menus/settings_menu.rb b/lib/sidebars/projects/menus/settings_menu.rb
index 85931e63ebc..11d5f4d59c7 100644
--- a/lib/sidebars/projects/menus/settings_menu.rb
+++ b/lib/sidebars/projects/menus/settings_menu.rb
@@ -13,6 +13,7 @@ module Sidebars
add_item(webhooks_menu_item)
add_item(access_tokens_menu_item)
add_item(repository_menu_item)
+ add_item(merge_requests_menu_item)
add_item(ci_cd_menu_item)
add_item(packages_and_registries_menu_item)
add_item(pages_menu_item)
@@ -109,9 +110,9 @@ module Sidebars
end
::Sidebars::MenuItem.new(
- title: _('Packages & Registries'),
+ title: _('Packages and registries'),
link: project_settings_packages_and_registries_path(context.project),
- active_routes: { path: 'packages_and_registries#show' },
+ active_routes: { controller: :packages_and_registries },
item_id: :packages_and_registries
)
end
@@ -150,6 +151,17 @@ module Sidebars
item_id: :usage_quotas
)
end
+
+ def merge_requests_menu_item
+ return unless context.project.merge_requests_enabled?
+
+ ::Sidebars::MenuItem.new(
+ title: _('Merge requests'),
+ link: project_settings_merge_requests_path(context.project),
+ active_routes: { path: 'projects/settings/merge_requests#show' },
+ item_id: :merge_requests
+ )
+ end
end
end
end
diff --git a/lib/sidebars/projects/panel.rb b/lib/sidebars/projects/panel.rb
index 1af8e14f034..8ae8f931aab 100644
--- a/lib/sidebars/projects/panel.rb
+++ b/lib/sidebars/projects/panel.rb
@@ -51,8 +51,7 @@ module Sidebars
end
def third_party_wiki_menu
- wiki_menu_list = [::Sidebars::Projects::Menus::ConfluenceMenu]
- wiki_menu_list << ::Sidebars::Projects::Menus::ShimoMenu if Feature.enabled?(:shimo_integration, context.project)
+ wiki_menu_list = [::Sidebars::Projects::Menus::ConfluenceMenu, ::Sidebars::Projects::Menus::ShimoMenu]
wiki_menu_list.find { |wiki_menu| wiki_menu.new(context).render? }
end
diff --git a/lib/tasks/gitlab/assets.rake b/lib/tasks/gitlab/assets.rake
index 0b70dba5c05..76ee5379213 100644
--- a/lib/tasks/gitlab/assets.rake
+++ b/lib/tasks/gitlab/assets.rake
@@ -40,6 +40,7 @@ module Tasks
asset_files
end
+
private_class_method :assets_impacting_webpack_compilation
end
end
@@ -84,9 +85,17 @@ namespace :gitlab do
if head_assets_sha256 != master_assets_sha256 || !public_assets_webpack_dir_exists
FileUtils.rm_r(Tasks::Gitlab::Assets::PUBLIC_ASSETS_WEBPACK_DIR) if public_assets_webpack_dir_exists
- unless system('yarn webpack')
+ log_path = ENV['WEBPACK_COMPILE_LOG_PATH']
+
+ cmd = 'yarn webpack'
+ cmd += " > #{log_path}" if log_path
+
+ unless system(cmd)
abort 'Error: Unable to compile webpack production bundle.'.color(:red)
end
+
+ puts "Written webpack stdout log to #{log_path}" if log_path
+ puts "You can inspect the webpack log here: #{ENV['CI_JOB_URL']}/artifacts/file/#{log_path}" if log_path && ENV['CI_JOB_URL']
end
end
diff --git a/lib/tasks/gitlab/db/truncate_legacy_tables.rake b/lib/tasks/gitlab/db/truncate_legacy_tables.rake
new file mode 100644
index 00000000000..9c3d7c3876d
--- /dev/null
+++ b/lib/tasks/gitlab/db/truncate_legacy_tables.rake
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+namespace :gitlab do
+ namespace :db do
+ namespace :truncate_legacy_tables do
+ desc "GitLab | DB | Truncate CI Tables on Main"
+ task :main, [:min_batch_size] => [:environment, 'gitlab:db:validate_config'] do |_t, args|
+ args.with_defaults(min_batch_size: 5)
+ Gitlab::Database::TablesTruncate.new(
+ database_name: 'main',
+ min_batch_size: args.min_batch_size.to_i,
+ logger: Logger.new($stdout),
+ dry_run: ENV['DRY_RUN'] == 'true',
+ until_table: ENV['UNTIL_TABLE']
+ ).execute
+ end
+
+ desc "GitLab | DB | Truncate Main Tables on CI"
+ task :ci, [:min_batch_size] => [:environment, 'gitlab:db:validate_config'] do |_t, args|
+ args.with_defaults(min_batch_size: 5)
+ Gitlab::Database::TablesTruncate.new(
+ database_name: 'ci',
+ min_batch_size: args.min_batch_size.to_i,
+ logger: Logger.new($stdout),
+ dry_run: ENV['DRY_RUN'] == 'true',
+ until_table: ENV['UNTIL_TABLE']
+ ).execute
+ end
+ end
+ end
+end
diff --git a/lib/tasks/gitlab/db/validate_config.rake b/lib/tasks/gitlab/db/validate_config.rake
index 2a3a54b5351..bf9ebc56486 100644
--- a/lib/tasks/gitlab/db/validate_config.rake
+++ b/lib/tasks/gitlab/db/validate_config.rake
@@ -144,7 +144,7 @@ namespace :gitlab do
rescue ActiveRecord::StatementInvalid => err
raise unless err.cause.is_a?(PG::ReadOnlySqlTransaction)
- warn "WARNING: Could not write to the database #{db_config.name}: #{err.message}"
+ warn "WARNING: Could not write to the database #{db_config.name}: cannot execute UPSERT in a read-only transaction"
end
def get_db_identifier(db_config)
diff --git a/lib/tasks/gitlab/import_export/export.rake b/lib/tasks/gitlab/import_export/export.rake
index 4bdc62c9319..3cefdcc1aaf 100644
--- a/lib/tasks/gitlab/import_export/export.rake
+++ b/lib/tasks/gitlab/import_export/export.rake
@@ -27,9 +27,9 @@ namespace :gitlab do
task = Gitlab::ImportExport::Project::ExportTask.new(
namespace_path: args.namespace_path,
- project_path: args.project_path,
- username: args.username,
- file_path: args.archive_path,
+ project_path: args.project_path,
+ username: args.username,
+ file_path: args.archive_path,
logger: logger
)
diff --git a/lib/tasks/gitlab/import_export/import.rake b/lib/tasks/gitlab/import_export/import.rake
index 2702b530334..fc727eda380 100644
--- a/lib/tasks/gitlab/import_export/import.rake
+++ b/lib/tasks/gitlab/import_export/import.rake
@@ -31,9 +31,9 @@ namespace :gitlab do
task = Gitlab::ImportExport::Project::ImportTask.new(
namespace_path: args.namespace_path,
- project_path: args.project_path,
- username: args.username,
- file_path: args.archive_path,
+ project_path: args.project_path,
+ username: args.username,
+ file_path: args.archive_path,
logger: logger
)
diff --git a/lib/tasks/gitlab/tw/codeowners.rake b/lib/tasks/gitlab/tw/codeowners.rake
index f6c518784a9..148801254bf 100644
--- a/lib/tasks/gitlab/tw/codeowners.rake
+++ b/lib/tasks/gitlab/tw/codeowners.rake
@@ -19,16 +19,15 @@ namespace :tw do
end
CODE_OWNER_RULES = [
- CodeOwnerRule.new('Activation', '@kpaizee'),
- CodeOwnerRule.new("Adoption", '@kpaizee'),
- CodeOwnerRule.new('Activation', '@kpaizee'),
- CodeOwnerRule.new('Adoption', '@kpaizee'),
+ CodeOwnerRule.new('Activation', '@phillipwells'),
+ CodeOwnerRule.new('Acquisition', '@phillipwells'),
+ CodeOwnerRule.new('Anti-Abuse', '@phillipwells'),
CodeOwnerRule.new('Authentication and Authorization', '@eread'),
CodeOwnerRule.new('Certify', '@msedlakjakubowski'),
CodeOwnerRule.new('Code Review', '@aqualls'),
CodeOwnerRule.new('Compliance', '@eread'),
CodeOwnerRule.new('Composition Analysis', '@rdickenson'),
- CodeOwnerRule.new('Configure', '@sselhorn'),
+ CodeOwnerRule.new('Configure', '@phillipwells'),
CodeOwnerRule.new('Container Security', '@claytoncornell'),
CodeOwnerRule.new('Contributor Experience', '@eread'),
CodeOwnerRule.new('Conversion', '@kpaizee'),
@@ -41,7 +40,6 @@ namespace :tw do
CodeOwnerRule.new('Dynamic Analysis', '@rdickenson'),
CodeOwnerRule.new('Ecosystem', '@kpaizee'),
CodeOwnerRule.new('Editor', '@aqualls'),
- CodeOwnerRule.new('Expansion', '@kpaizee'),
CodeOwnerRule.new('Foundations', '@rdickenson'),
CodeOwnerRule.new('Fuzz Testing', '@rdickenson'),
CodeOwnerRule.new('Geo', '@axil'),
diff --git a/lib/tasks/gitlab/uploads/migrate.rake b/lib/tasks/gitlab/uploads/migrate.rake
index 80290f95e8e..2a91fd1646c 100644
--- a/lib/tasks/gitlab/uploads/migrate.rake
+++ b/lib/tasks/gitlab/uploads/migrate.rake
@@ -2,15 +2,8 @@
namespace :gitlab do
namespace :uploads do
- namespace :migrate do
- desc "GitLab | Uploads | Migrate all uploaded files to object storage"
- task all: :environment do
- Gitlab::Uploads::MigrationHelper.categories.each do |args|
- Rake::Task["gitlab:uploads:migrate"].invoke(*args)
- Rake::Task["gitlab:uploads:migrate"].reenable
- end
- end
- end
+ desc "GitLab | Uploads | Migrate all uploaded files to object storage"
+ task 'migrate:all' => :migrate
# The following is the actual rake task that migrates uploads of specified
# category to object storage
@@ -19,15 +12,8 @@ namespace :gitlab do
Gitlab::Uploads::MigrationHelper.new(args, Logger.new($stdout)).migrate_to_remote_storage
end
- namespace :migrate_to_local do
- desc "GitLab | Uploads | Migrate all uploaded files to local storage"
- task all: :environment do
- Gitlab::Uploads::MigrationHelper.categories.each do |args|
- Rake::Task["gitlab:uploads:migrate_to_local"].invoke(*args)
- Rake::Task["gitlab:uploads:migrate_to_local"].reenable
- end
- end
- end
+ desc "GitLab | Uploads | Migrate all uploaded files to local storage"
+ task 'migrate_to_local:all' => :migrate_to_local
desc 'GitLab | Uploads | Migrate the uploaded files of specified type to local storage'
task :migrate_to_local, [:uploader_class, :model_class, :mounted_as] => :environment do |_t, args|
diff --git a/lib/tasks/gitlab/usage_data.rake b/lib/tasks/gitlab/usage_data.rake
index 9f064ef4c0c..73a79427da3 100644
--- a/lib/tasks/gitlab/usage_data.rake
+++ b/lib/tasks/gitlab/usage_data.rake
@@ -17,11 +17,16 @@ namespace :gitlab do
puts Gitlab::Json.pretty_generate(Gitlab::Usage::ServicePingReport.for(output: :all_metrics_values))
end
+ desc 'GitLab | UsageData | Generate non SQL data for usage ping in JSON'
+ task dump_non_sql_in_json: :environment do
+ puts Gitlab::Json.pretty_generate(Gitlab::Usage::ServicePingReport.for(output: :non_sql_metrics_values))
+ end
+
desc 'GitLab | UsageData | Generate usage ping and send it to Versions Application'
task generate_and_send: :environment do
- result = ServicePing::SubmitService.new.execute
+ response = GitlabServicePingWorker.new.perform('triggered_from_cron' => false)
- puts Gitlab::Json.pretty_generate(result.attributes)
+ puts response.body, response.code, response.message, response.headers.inspect
end
desc 'GitLab | UsageDataMetrics | Generate usage ping from metrics definition YAML files in JSON'
@@ -51,6 +56,19 @@ namespace :gitlab do
File.write(Gitlab::UsageDataCounters::CiTemplateUniqueCounter::KNOWN_EVENTS_FILE_PATH, banner + YAML.dump(all_includes).gsub(/ *$/m, ''))
end
+ desc 'GitLab | UsageDataMetrics | Generate raw SQL metrics queries for RSpec'
+ task generate_sql_metrics_queries: :environment do
+ path = Rails.root.join('tmp', 'test')
+
+ queries = Timecop.freeze(2021, 1, 1) do
+ Gitlab::Usage::ServicePingReport.for(output: :metrics_queries)
+ end
+
+ FileUtils.mkdir_p(path)
+ FileUtils.chdir(path)
+ File.write('sql_metrics_queries.json', Gitlab::Json.pretty_generate(queries))
+ end
+
def ci_template_includes_hash(source, template_directory = nil)
Gitlab::UsageDataCounters::CiTemplateUniqueCounter.ci_templates("lib/gitlab/ci/templates/#{template_directory}").map do |template|
expanded_template_name = Gitlab::UsageDataCounters::CiTemplateUniqueCounter.expand_template_name("#{template_directory}/#{template}")
diff --git a/lib/tasks/haml-lint.rake b/lib/tasks/haml-lint.rake
deleted file mode 100644
index 29589571344..00000000000
--- a/lib/tasks/haml-lint.rake
+++ /dev/null
@@ -1,7 +0,0 @@
-# frozen_string_literal: true
-
-unless Rails.env.production?
- require 'haml_lint/rake_task'
-
- HamlLint::RakeTask.new
-end
diff --git a/lib/tasks/rubocop.rake b/lib/tasks/rubocop.rake
index e993035aa65..0c257585bd0 100644
--- a/lib/tasks/rubocop.rake
+++ b/lib/tasks/rubocop.rake
@@ -6,6 +6,19 @@ unless Rails.env.production?
RuboCop::RakeTask.new
namespace :rubocop do
+ namespace :check do
+ desc 'Run RuboCop check gracefully'
+ task :graceful do |_task, args|
+ require_relative '../../rubocop/check_graceful_task'
+
+ # Don't reveal TODOs in this run.
+ ENV.delete('REVEAL_RUBOCOP_TODO')
+
+ result = RuboCop::CheckGracefulTask.new($stdout).run(args.extras)
+ exit result if result.nonzero?
+ end
+ end
+
namespace :todo do
desc 'Generate RuboCop todos'
task :generate do |_task, args|
diff --git a/lib/tasks/tanuki_emoji.rake b/lib/tasks/tanuki_emoji.rake
index 0dc7dd4e701..b02d7a532c4 100644
--- a/lib/tasks/tanuki_emoji.rake
+++ b/lib/tasks/tanuki_emoji.rake
@@ -148,11 +148,11 @@ namespace :tanuki_emoji do
SpriteFactory.run!(tmpdir, {
output_style: style_path,
output_image: "app/assets/images/emoji.png",
- selector: '.emoji-',
- style: :scss,
- nocomments: true,
- pngcrush: true,
- layout: :packed
+ selector: '.emoji-',
+ style: :scss,
+ nocomments: true,
+ pngcrush: true,
+ layout: :packed
})
# SpriteFactory's SCSS is a bit too verbose for our purposes here, so
@@ -215,10 +215,10 @@ namespace :tanuki_emoji do
# Combine the resized assets into a packed sprite and re-generate the SCSS
SpriteFactory.run!(tmpdir, {
output_image: "app/assets/images/emoji@2x.png",
- style: false,
- nocomments: true,
- pngcrush: true,
- layout: :packed
+ style: false,
+ nocomments: true,
+ pngcrush: true,
+ layout: :packed
})
end