Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2025-04-23 15:19:55 +00:00
parent 6a7ae91bd0
commit ebb15c08a9
85 changed files with 994 additions and 254 deletions

View File

@ -2,6 +2,23 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
## 17.11.1 (2025-04-22)
### Fixed (1 change)
- [Fix string conversion for CI Inputs](https://gitlab.com/gitlab-org/security/gitlab/-/commit/aceb71126fb8ea5be6259a2156c6255bbaa1f3de)
### Changed (1 change)
- [Put allow_composite_identities_to_run_pipelines behind ff](https://gitlab.com/gitlab-org/security/gitlab/-/commit/2287e37df7c9ed82aa54643759e00bbf30a788c8)
### Security (4 changes)
- [Add SecureHeaders middleware with the Nel header](https://gitlab.com/gitlab-org/security/gitlab/-/commit/5a586de4d56429eabe0fb6ebc524894925759d2e) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4934))
- [Restrict forwarded headers in Maven dependency proxy](https://gitlab.com/gitlab-org/security/gitlab/-/commit/80244b98dd92312510f4a9276b5adfcbaba8e68a) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4929))
- [Security unauthorized access to reading branch names](https://gitlab.com/gitlab-org/security/gitlab/-/commit/9f9724584d109181e764f79a3b61667520d2212f) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4933))
- [Simplify detecting paragraphs for quick actions](https://gitlab.com/gitlab-org/security/gitlab/-/commit/78466ef2cd3ddee5fbf0db67056a5bccf7c59907) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4925))
## 17.11.0 (2025-04-16)
### Added (211 changes)
@ -767,6 +784,25 @@ entry.
- [Remove feature flag allow_merge_request_pipelines_from_fork](https://gitlab.com/gitlab-org/gitlab/-/commit/b62f9187a57cc5ba66ce26889516cc55a425181a) ([merge request](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/182862))
- [Finalize migration BackfillNewAuditEventTables](https://gitlab.com/gitlab-org/gitlab/-/commit/1bc0f07ffd3af5b9fab8a0ea0b1af5f2759d25db) ([merge request](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/181881))
## 17.10.5 (2025-04-22)
### Fixed (3 changes)
- [Fix 500 in Todo API when wiki page todo exists](https://gitlab.com/gitlab-org/security/gitlab/-/commit/71215f0615fad3167fb96b521b9628e11ea30a5d)
- [Clear session cookie when browser is closed](https://gitlab.com/gitlab-org/security/gitlab/-/commit/7c77ca404d9be7166d8ef991013394483b3f0371)
- [Fix workspaces reconciliation to send inventory config map correctly](https://gitlab.com/gitlab-org/security/gitlab/-/commit/aba508e925aea81c4d47555254e6a657edc94863) **GitLab Enterprise Edition**
### Security (4 changes)
- [Add SecureHeaders middleware with the Nel header](https://gitlab.com/gitlab-org/security/gitlab/-/commit/0e180be62768513438f86ea99f0a4a305cca46b6) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4919))
- [Restrict forwarded headers in Maven dependency proxy](https://gitlab.com/gitlab-org/security/gitlab/-/commit/faa100503f89d08e51549e4f35f362c9945dbb6f) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4930))
- [Security unauthorized access to reading branch names](https://gitlab.com/gitlab-org/security/gitlab/-/commit/dc2f917499f58ed9ccff23158b39528b62b71c2f) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4895))
- [Simplify detecting paragraphs for quick actions](https://gitlab.com/gitlab-org/security/gitlab/-/commit/507e465f21b5be5297eda7a67f0ba75994df88d8) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4926))
### Other (1 change)
- [No-op FinalizeBackfillCiRunnerMachinesPartitionedTable migration](https://gitlab.com/gitlab-org/security/gitlab/-/commit/119891459658f48120bcef02b1b66e3e78c78865)
## 17.10.4 (2025-04-09)
### Fixed (2 changes)
@ -1608,6 +1644,20 @@ No changes.
- [Quarantine a flaky test](https://gitlab.com/gitlab-org/gitlab/-/commit/998d8028213da6bf0c3c1c08301797c8b3395c28) ([merge request](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/180234))
- [Quarantine a flaky test](https://gitlab.com/gitlab-org/gitlab/-/commit/8ae69a3765cfb7561db95e43faa30cc60fac6444) ([merge request](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/177662))
## 17.9.7 (2025-04-22)
### Security (4 changes)
- [Add SecureHeaders middleware with the Nel header](https://gitlab.com/gitlab-org/security/gitlab/-/commit/5e9112ba2d6fac340e1dbfc4b0330bdcff2f82bc) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4920))
- [Restrict forwarded headers in Maven dependency proxy](https://gitlab.com/gitlab-org/security/gitlab/-/commit/24d73f3e778471c4e0153af174f2d8b44e106108) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4931))
- [Security unauthorized access to reading branch names](https://gitlab.com/gitlab-org/security/gitlab/-/commit/cb09987371857b97ec3cc5774b9f214c9db7c8a6) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4923))
- [Simplify detecting paragraphs for quick actions](https://gitlab.com/gitlab-org/security/gitlab/-/commit/29d3f746b02137aaff4364473ce62f701d208e27) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4927))
### Other (2 changes)
- [Clean up BackfillCiRunnerMachinesPartitionedTable migration](https://gitlab.com/gitlab-org/security/gitlab/-/commit/76d351431e07b3a695356c7b78ac16d23b180302)
- [No-op FinalizeBackfillCiRunnerMachinesPartitionedTable migration](https://gitlab.com/gitlab-org/security/gitlab/-/commit/fb16e2ebcc6188b3f9bb0bc30e7e2b709484f3b1)
## 17.9.6 (2025-04-09)
### Security (6 changes)

View File

@ -117,11 +117,12 @@ const watchFluxResource = async ({
};
try {
const config = new Configuration(variables.configuration);
await subscribeToSocket({
watchId,
watchParams,
configuration: variables.configuration,
cacheParams,
config,
});
} catch {
await watchFunction();

View File

@ -0,0 +1,18 @@
import { WebSocketWatchManager } from '@gitlab/cluster-client';
let watchManagerInstance = null;
export function resetWatchManager() {
watchManagerInstance = null;
}
export function getWatchManager(configuration) {
if (!watchManagerInstance) {
if (!configuration) {
throw new Error('WebSocketWatchManager not initialized. Provide configuration first.');
}
watchManagerInstance = new WebSocketWatchManager(configuration);
}
return watchManagerInstance;
}

View File

@ -2,11 +2,11 @@ import {
CoreV1Api,
Configuration,
WatchApi,
webSocketWatchManager,
EVENT_DATA,
EVENT_TIMEOUT,
EVENT_ERROR,
} from '@gitlab/cluster-client';
import { getWatchManager } from '~/environments/services/websocket_connection_service';
import { connectionStatus } from '~/environments/graphql/resolvers/kubernetes/constants';
import { updateConnectionStatus } from '~/environments/graphql/resolvers/kubernetes/k8s_connection_status';
import { s__ } from '~/locale';
@ -54,13 +54,14 @@ export const mapEventItem = ({
type,
}) => ({ lastTimestamp, eventTime, message, reason, source, type });
export const subscribeToSocket = async ({ watchId, watchParams, configuration, cacheParams }) => {
export const subscribeToSocket = async ({ watchId, watchParams, cacheParams, config }) => {
const { updateQueryCache, updateConnectionStatusFn } = cacheParams;
try {
const watcher = await webSocketWatchManager.initConnection({
const watcherConnection = getWatchManager(config);
const watcher = await watcherConnection.initConnection({
message: { watchId, watchParams },
configuration,
});
const handleConnectionStatus = (status) => {
@ -142,7 +143,7 @@ export const watchWorkloadItems = async ({
};
try {
await subscribeToSocket({ watchId, watchParams, configuration, cacheParams });
await subscribeToSocket({ watchId, watchParams, cacheParams, config });
} catch {
await watchFunction();
}

View File

@ -6,14 +6,14 @@ import * as Sentry from '~/sentry/sentry_browser_wrapper';
import Tracking from '~/tracking';
import { __ } from '~/locale';
import { clearDraft } from '~/lib/utils/autosave';
import { findWidget } from '~/issues/list/utils';
import DiscussionReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue';
import ResolveDiscussionButton from '~/notes/components/discussion_resolve_button.vue';
import { updateCacheAfterCreatingNote } from '../../graphql/cache_utils';
import createNoteMutation from '../../graphql/notes/create_work_item_note.mutation.graphql';
import workItemNotesByIidQuery from '../../graphql/notes/work_item_notes_by_iid.query.graphql';
import workItemByIidQuery from '../../graphql/work_item_by_iid.query.graphql';
import { TRACKING_CATEGORY_SHOW, WIDGET_TYPE_EMAIL_PARTICIPANTS, i18n } from '../../constants';
import { TRACKING_CATEGORY_SHOW, i18n } from '../../constants';
import { findEmailParticipantsWidget } from '../../utils';
import WorkItemNoteSignedOut from './work_item_note_signed_out.vue';
import WorkItemCommentLocked from './work_item_comment_locked.vue';
import WorkItemCommentForm from './work_item_comment_form.vue';
@ -223,7 +223,7 @@ export default {
return this.isDiscussionResolved ? __('Unresolve thread') : __('Resolve thread');
},
hasEmailParticipantsWidget() {
return Boolean(findWidget(WIDGET_TYPE_EMAIL_PARTICIPANTS, this.workItem));
return Boolean(findEmailParticipantsWidget(this.workItem));
},
},
watch: {

View File

@ -3,14 +3,8 @@ import { GlButton, GlFormCheckbox, GlTooltipDirective } from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper';
import { s__, __ } from '~/locale';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
import {
STATE_OPEN,
WORK_ITEM_TYPE_NAME_TASK,
WIDGET_TYPE_EMAIL_PARTICIPANTS,
i18n,
} from '~/work_items/constants';
import { STATE_OPEN, WORK_ITEM_TYPE_NAME_TASK, i18n } from '~/work_items/constants';
import { getDraft, clearDraft, updateDraft } from '~/lib/utils/autosave';
import { findWidget } from '~/issues/list/utils';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import glAbilitiesMixin from '~/vue_shared/mixins/gl_abilities_mixin';
import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
@ -19,6 +13,7 @@ import WorkItemStateToggle from '~/work_items/components/work_item_state_toggle.
import CommentFieldLayout from '~/notes/components/comment_field_layout.vue';
import workItemByIidQuery from '../../graphql/work_item_by_iid.query.graphql';
import workItemEmailParticipantsByIidQuery from '../../graphql/notes/work_item_email_participants_by_iid.query.graphql';
import { findEmailParticipantsWidget } from '../../utils';
const DOCS_WORK_ITEM_LOCKED_TASKS_PATH = helpPagePath('user/tasks.html', {
anchor: 'lock-discussion',
@ -269,8 +264,7 @@ export default {
},
update(data) {
return (
findWidget(WIDGET_TYPE_EMAIL_PARTICIPANTS, data?.workspace?.workItem)?.emailParticipants
?.nodes || []
findEmailParticipantsWidget(data?.workspace?.workItem)?.emailParticipants?.nodes || []
);
},
},

View File

@ -2,9 +2,7 @@
import { GlTooltipDirective, GlAvatarsInline, GlAvatar, GlAvatarLink } from '@gitlab/ui';
import ItemMilestone from '~/issuable/components/issue_milestone.vue';
import { s__, sprintf } from '~/locale';
import { WIDGET_TYPE_MILESTONE, WIDGET_TYPE_ASSIGNEES } from '~/work_items/constants';
import { findWidget } from '~/issues/list/utils';
import { getDisplayReference } from '../../utils';
import { findAssigneesWidget, findMilestoneWidget, getDisplayReference } from '../../utils';
export default {
name: 'WorkItemRelationshipPopoverMetadata',
@ -30,10 +28,10 @@ export default {
assigneesDisplayLimit: 3,
computed: {
workItemAssignees() {
return findWidget(WIDGET_TYPE_ASSIGNEES, this.workItem)?.assignees?.nodes || [];
return findAssigneesWidget(this.workItem)?.assignees?.nodes || [];
},
workItemMilestone() {
return findWidget(WIDGET_TYPE_MILESTONE, this.workItem)?.milestone;
return findMilestoneWidget(this.workItem)?.milestone;
},
fullReference() {
return getDisplayReference(this.workItemFullPath, this.workItem.reference);

View File

@ -1,7 +1,6 @@
<script>
import { GlLink, GlPopover, GlTooltipDirective, GlTruncateText } from '@gitlab/ui';
import { difference, groupBy, xor } from 'lodash';
import { findWidget } from '~/issues/list/utils';
import { __, n__, s__ } from '~/locale';
import WorkItemSidebarDropdownWidget from '~/work_items/components/shared/work_item_sidebar_dropdown_widget.vue';
import Tracking from '~/tracking';
@ -9,8 +8,8 @@ import getGroupContactsQuery from '~/crm/contacts/components/graphql/get_group_c
import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql';
import updateNewWorkItemMutation from '../graphql/update_new_work_item.mutation.graphql';
import { i18n, TRACKING_CATEGORY_SHOW, WIDGET_TYPE_CRM_CONTACTS } from '../constants';
import { newWorkItemFullPath, newWorkItemId } from '../utils';
import { i18n, TRACKING_CATEGORY_SHOW } from '../constants';
import { findCrmContactsWidget, newWorkItemFullPath, newWorkItemId } from '../utils';
export default {
directives: {
@ -86,7 +85,7 @@ export default {
return this.groupByOrganization(this.selectedItems, false);
},
workItemCrmContacts() {
return findWidget(WIDGET_TYPE_CRM_CONTACTS, this.workItem);
return findCrmContactsWidget(this.workItem);
},
workItemFullPath() {
return this.createFlow

View File

@ -1,20 +1,12 @@
<script>
import { GlIcon, GlAlert, GlTooltipDirective } from '@gitlab/ui';
import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { s__, __ } from '~/locale';
import { findWidget } from '~/issues/list/utils';
import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
import workItemDevelopmentQuery from '~/work_items/graphql/work_item_development.query.graphql';
import workItemDevelopmentUpdatedSubscription from '~/work_items/graphql/work_item_development.subscription.graphql';
import {
sprintfWorkItem,
WIDGET_TYPE_DEVELOPMENT,
STATE_OPEN,
DEVELOPMENT_ITEMS_ANCHOR,
} from '~/work_items/constants';
import { sprintfWorkItem, STATE_OPEN, DEVELOPMENT_ITEMS_ANCHOR } from '~/work_items/constants';
import { findDevelopmentWidget } from '~/work_items/utils';
import CrudComponent from '~/vue_shared/components/crud_component.vue';
import WorkItemActionsSplitButton from '~/work_items/components/work_item_links/work_item_actions_split_button.vue';
import WorkItemDevelopmentRelationshipList from './work_item_development_relationship_list.vue';
@ -204,7 +196,7 @@ export default {
};
},
update(data) {
return findWidget(WIDGET_TYPE_DEVELOPMENT, data?.workItem) || {};
return findDevelopmentWidget(data?.workItem) || {};
},
skip() {
return !this.workItemIid;

View File

@ -2,13 +2,8 @@
import { GlIcon, GlTooltip, GlPopover } from '@gitlab/ui';
import { s__, __ } from '~/locale';
import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
import { findWidget } from '~/issues/list/utils';
import {
i18n,
WIDGET_TYPE_WEIGHT,
WORK_ITEM_TYPE_NAME_EPIC,
WIDGET_TYPE_HEALTH_STATUS,
} from '../../constants';
import { i18n, WORK_ITEM_TYPE_NAME_EPIC } from '../../constants';
import { findHealthStatusWidget, findWeightWidget } from '../../utils';
export default {
components: {
@ -72,10 +67,10 @@ export default {
},
computed: {
workItemWeight() {
return findWidget(WIDGET_TYPE_WEIGHT, this.workItem);
return findWeightWidget(this.workItem);
},
workItemHealthStatus() {
return findWidget(WIDGET_TYPE_HEALTH_STATUS, this.workItem);
return findHealthStatusWidget(this.workItem);
},
shouldRolledUpWeightBeVisible() {
return this.showRolledUpWeight && this.rolledUpWeight !== null;

View File

@ -3,7 +3,6 @@ import { GlAlert } from '@gitlab/ui';
import { sprintf, s__ } from '~/locale';
import { createAlert } from '~/alert';
import CrudComponent from '~/vue_shared/components/crud_component.vue';
import { findWidget } from '~/issues/list/utils';
import { getParameterByName } from '~/lib/utils/url_utility';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import {
@ -192,7 +191,7 @@ export default {
},
computed: {
workItemHierarchy() {
return findWidget(WIDGET_TYPE_HIERARCHY, this.workItem);
return findHierarchyWidget(this.workItem);
},
rolledUpCountsByType() {
return this.workItemHierarchy?.rolledUpCountsByType || [];

View File

@ -8,7 +8,6 @@ import { convertToGraphQLId } from '~/graphql_shared/utils';
import { getBaseURL } from '~/lib/utils/url_utility';
import { convertEachWordToTitleCase } from '~/lib/utils/text_utility';
import { getDraft, clearDraft } from '~/lib/utils/autosave';
import { findWidget } from '~/issues/list/utils';
import {
newWorkItemOptimisticUserPermissions,
WIDGET_TYPE_ASSIGNEES,
@ -32,6 +31,7 @@ import {
} from 'ee_else_ce/work_items/constants';
import {
findCurrentUserTodosWidget,
findDescriptionWidget,
findHierarchyWidget,
findHierarchyWidgetChildren,
findNotesWidget,
@ -350,8 +350,7 @@ export const setNewWorkItemCache = async (
const draftData = JSON.parse(getDraft(autosaveKey));
const draftTitle = draftData?.workspace?.workItem?.title || '';
const draftDescriptionWidget =
findWidget(WIDGET_TYPE_DESCRIPTION, draftData?.workspace?.workItem) || {};
const draftDescriptionWidget = findDescriptionWidget(draftData?.workspace?.workItem) || {};
const draftDescription = draftDescriptionWidget?.description || null;
widgets.push({

View File

@ -5,7 +5,12 @@ import { findWidget } from '~/issues/list/utils';
import { newDate, toISODateFormat } from '~/lib/utils/datetime_utility';
import { updateDraft } from '~/lib/utils/autosave';
import { getParameterByName } from '~/lib/utils/url_utility';
import { getNewWorkItemAutoSaveKey, newWorkItemFullPath } from '../utils';
import {
findCustomFieldsWidget,
findStartAndDueDateWidget,
getNewWorkItemAutoSaveKey,
newWorkItemFullPath,
} from '../utils';
import {
WIDGET_TYPE_ASSIGNEES,
WIDGET_TYPE_COLOR,
@ -15,7 +20,6 @@ import {
WIDGET_TYPE_CRM_CONTACTS,
WIDGET_TYPE_ITERATION,
WIDGET_TYPE_WEIGHT,
WIDGET_TYPE_START_AND_DUE_DATE,
NEW_WORK_ITEM_IID,
WIDGET_TYPE_MILESTONE,
WIDGET_TYPE_HIERARCHY,
@ -44,7 +48,7 @@ const updateDatesWidget = (draftData, dates) => {
const dueDate = dates.dueDate ? toISODateFormat(newDate(dates.dueDate)) : null;
const startDate = dates.startDate ? toISODateFormat(newDate(dates.startDate)) : null;
const widget = findWidget(WIDGET_TYPE_START_AND_DUE_DATE, draftData.workspace.workItem);
const widget = findStartAndDueDateWidget(draftData.workspace.workItem);
Object.assign(widget, {
dueDate,
startDate,
@ -57,7 +61,7 @@ const updateDatesWidget = (draftData, dates) => {
const updateCustomFieldsWidget = (sourceData, draftData, customField) => {
if (!customField) return;
const widget = findWidget(WIDGET_TYPE_CUSTOM_FIELDS, draftData.workspace.workItem);
const widget = findCustomFieldsWidget(draftData.workspace.workItem);
if (!widget) return;

View File

@ -1,37 +1,42 @@
import { escapeRegExp } from 'lodash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { queryToObject, joinPaths } from '~/lib/utils/url_utility';
import { joinPaths, queryToObject } from '~/lib/utils/url_utility';
import AccessorUtilities from '~/lib/utils/accessor';
import { parseBoolean } from '~/lib/utils/common_utils';
import { TYPE_EPIC, TYPE_ISSUE } from '~/issues/constants';
import {
DEFAULT_PAGE_SIZE_CHILD_ITEMS,
ISSUABLE_EPIC,
NAME_TO_ENUM_MAP,
NEW_WORK_ITEM_GID,
NEW_WORK_ITEM_IID,
STATE_CLOSED,
WIDGET_TYPE_ASSIGNEES,
WIDGET_TYPE_AWARD_EMOJI,
WIDGET_TYPE_COLOR,
WIDGET_TYPE_CRM_CONTACTS,
WIDGET_TYPE_CURRENT_USER_TODOS,
WIDGET_TYPE_CUSTOM_FIELDS,
WIDGET_TYPE_DESCRIPTION,
WIDGET_TYPE_DESIGNS,
WIDGET_TYPE_DEVELOPMENT,
WIDGET_TYPE_EMAIL_PARTICIPANTS,
WIDGET_TYPE_ERROR_TRACKING,
WIDGET_TYPE_HEALTH_STATUS,
WIDGET_TYPE_HIERARCHY,
WIDGET_TYPE_ITERATION,
WIDGET_TYPE_LABELS,
WIDGET_TYPE_LINKED_ITEMS,
WIDGET_TYPE_MILESTONE,
WIDGET_TYPE_NOTES,
WIDGET_TYPE_START_AND_DUE_DATE,
WIDGET_TYPE_STATUS,
WIDGET_TYPE_TIME_TRACKING,
WIDGET_TYPE_VULNERABILITIES,
WIDGET_TYPE_WEIGHT,
ISSUABLE_EPIC,
WORK_ITEMS_TYPE_MAP,
WORK_ITEM_TYPE_ENUM_EPIC,
WORK_ITEM_TYPE_ROUTE_WORK_ITEM,
NEW_WORK_ITEM_GID,
DEFAULT_PAGE_SIZE_CHILD_ITEMS,
STATE_CLOSED,
NAME_TO_ENUM_MAP,
WIDGET_TYPE_DEVELOPMENT,
WORK_ITEMS_TYPE_MAP,
} from './constants';
export const isAssigneesWidget = (widget) => widget.type === WIDGET_TYPE_ASSIGNEES;
@ -40,23 +45,35 @@ export const isMilestoneWidget = (widget) => widget.type === WIDGET_TYPE_MILESTO
export const isNotesWidget = (widget) => widget.type === WIDGET_TYPE_NOTES;
export const findAssigneesWidget = (workItem) =>
workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_ASSIGNEES);
export const findAwardEmojiWidget = (workItem) =>
workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_AWARD_EMOJI);
export const findColorWidget = (workItem) =>
workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_COLOR);
export const findCrmContactsWidget = (workItem) =>
workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_CRM_CONTACTS);
export const findCurrentUserTodosWidget = (workItem) =>
workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_CURRENT_USER_TODOS);
export const findCustomFieldsWidget = (workItem) =>
workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_CUSTOM_FIELDS);
export const findDescriptionWidget = (workItem) =>
workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_DESCRIPTION);
export const findDesignsWidget = (workItem) =>
workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_DESIGNS);
export const findDevelopmentWidget = (workItem) =>
workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_DEVELOPMENT);
export const findDesignsWidget = (workItem) =>
workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_DESIGNS);
export const findEmailParticipantsWidget = (workItem) =>
workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_EMAIL_PARTICIPANTS);
export const findErrorTrackingWidget = (workItem) =>
workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_ERROR_TRACKING);
@ -67,6 +84,9 @@ export const findHealthStatusWidget = (workItem) =>
export const findHierarchyWidget = (workItem) =>
workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_HIERARCHY);
export const findIterationWidget = (workItem) =>
workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_ITERATION);
export const findLabelsWidget = (workItem) =>
workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_LABELS);
@ -82,9 +102,15 @@ export const findNotesWidget = (workItem) =>
export const findStartAndDueDateWidget = (workItem) =>
workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_START_AND_DUE_DATE);
export const findStatusWidget = (workItem) =>
workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_STATUS);
export const findTimeTrackingWidget = (workItem) =>
workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_TIME_TRACKING);
export const findVulnerabilitiesWidget = (workItem) =>
workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_VULNERABILITIES);
export const findWeightWidget = (workItem) =>
workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_WEIGHT);

View File

@ -144,7 +144,7 @@ module Groups
crm_enabled = params.delete(:crm_enabled)
crm_enabled = true if crm_enabled.nil?
crm_source_group_id = params.delete(:crm_source_group_id)
crm_source_group_id = params.delete(:crm_source_group_id).presence
return if group.crm_enabled? == crm_enabled && group.crm_settings&.source_group_id == crm_source_group_id
if group.crm_settings&.source_group_id != crm_source_group_id && group.has_issues_with_contacts?
@ -154,7 +154,7 @@ module Groups
crm_settings = group.crm_settings || group.build_crm_settings
crm_settings.enabled = crm_enabled
crm_settings.source_group_id = crm_source_group_id.presence
crm_settings.source_group_id = crm_source_group_id
crm_settings.save
end

View File

@ -68,7 +68,7 @@ module Projects
attr_reader :current_user, :params
def user_permitted?
Ability.allowed?(current_user, :admin_project, project)
current_user.can?(:import_projects, project.namespace) || current_user.can?(:admin_project, project)
end
def relation_valid?

View File

@ -91,7 +91,7 @@ module Projects
relation_factory: Gitlab::ImportExport::Project::RelationFactory,
reader: Gitlab::ImportExport::Reader.new(shared: project.import_export_shared),
importable: project,
importable_attributes: relation_reader.consume_attributes('project'),
importable_attributes: {},
importable_path: 'project',
skip_on_duplicate_iid: true
)
@ -113,12 +113,15 @@ module Projects
Gitlab::ImportExport::MembersMapper.new(
exported_members: project_members,
user: current_user,
importable: project
importable: project,
default_member: false
)
end
def perform_post_import_tasks
project.reset_counters_and_iids
# Issue iids are scoped on :namespace so that scope must be flushed as well
InternalId.flush_records!(namespace: project.project_namespace)
end
def log_failure(exception)

View File

@ -82,6 +82,7 @@ module Gitlab
require_dependency Rails.root.join('lib/gitlab/middleware/handle_malformed_strings')
require_dependency Rails.root.join('lib/gitlab/middleware/path_traversal_check')
require_dependency Rails.root.join('lib/gitlab/middleware/rack_multipart_tempfile_factory')
require_dependency Rails.root.join('lib/gitlab/middleware/secure_headers')
require_dependency Rails.root.join('lib/gitlab/runtime')
require_dependency Rails.root.join('lib/gitlab/patch/database_config')
require_dependency Rails.root.join('lib/gitlab/patch/redis_cache_store')
@ -444,6 +445,8 @@ module Gitlab
config.middleware.insert_before Rack::Runtime, ::Gitlab::Middleware::CompressedJson
config.middleware.insert_after ActionDispatch::Cookies, ::Gitlab::Middleware::SecureHeaders
# Allow access to GitLab API from other domains
config.middleware.insert_before Warden::Manager, Rack::Cors do
headers_to_expose = %w[Link X-Total X-Total-Pages X-Per-Page X-Page X-Next-Page X-Prev-Page X-Gitlab-Blob-Id X-Gitlab-Commit-Id X-Gitlab-Content-Sha256 X-Gitlab-Encoding X-Gitlab-File-Name X-Gitlab-File-Path X-Gitlab-Last-Commit-Id X-Gitlab-Ref X-Gitlab-Size X-Request-Id ETag]

View File

@ -37,6 +37,12 @@ en:
grafana_enabled: "Grafana integration enabled"
service_desk_setting:
project_key: "Project name suffix"
system_access/group_microsoft_application:
tenant_xid: "Tenant ID"
client_xid: "Client ID"
encrypted_client_secret: "Client secret"
login_endpoint: "Login API endpoint"
graph_endpoint: "Graph API endpoint"
system_access/microsoft_application:
tenant_xid: "Tenant ID"
client_xid: "Client ID"

View File

@ -172,7 +172,7 @@ Settings = GitlabSettings.load(file, Rails.env) do
# generate a hash of the password:
# https://github.com/attr-encrypted/encryptor/blob/c3a62c4a9e74686dd95e0548f9dc2a361fdc95d1/lib/encryptor.rb#L77
def db_key_base_keys
Array(Gitlab::Application.credentials.db_key_base).tap do |keys|
@db_key_base_keys ||= Array(Gitlab::Application.credentials.db_key_base).tap do |keys|
raise(MultipleDbKeyBaseError, "Defining multiple `db_key_base` keys isn't supported yet.") if keys.size > 1
end
end

View File

@ -5,4 +5,4 @@ feature_category: system_access
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/177531
milestone: '17.9'
queued_migration_version: 20250127052138
finalized_by: # version of the migration that finalized this BBM
finalized_by: 20250416005514

View File

@ -0,0 +1,14 @@
# frozen_string_literal: true
class DropNotNullConstraintFromProjectFingerprint < Gitlab::Database::Migration[2.2]
disable_ddl_transaction!
milestone '18.0'
def up
change_column_null :vulnerability_occurrences, :project_fingerprint, true
change_column_null :vulnerability_feedback, :project_fingerprint, true
end
def down
# no-op
end
end

View File

@ -0,0 +1,23 @@
# frozen_string_literal: true
class FinalizeSplitMicrosoftApplicationsTable < Gitlab::Database::Migration[2.2]
milestone '18.0'
disable_ddl_transaction!
restrict_gitlab_migration gitlab_schema: :gitlab_main
MIGRATION = "SplitMicrosoftApplicationsTable"
def up
ensure_batched_background_migration_is_finished(
job_class_name: MIGRATION,
table_name: :system_access_microsoft_applications,
column_name: :id,
job_arguments: [],
finalize: true
)
end
def down; end
end

View File

@ -0,0 +1 @@
380c2a8f4619d082324e81b281becd510b89391458c4dd5a6e5767a47d50e1e8

View File

@ -0,0 +1 @@
633ac4a40eee2fc8d4bdfeca223e8308cfe68d6fea52f7b84abdbfd1975efdbc

View File

@ -25026,7 +25026,7 @@ CREATE TABLE vulnerability_feedback (
project_id bigint NOT NULL,
author_id bigint NOT NULL,
issue_id bigint,
project_fingerprint character varying(40) NOT NULL,
project_fingerprint character varying(40),
merge_request_id bigint,
comment_author_id bigint,
comment text,
@ -25333,7 +25333,7 @@ CREATE TABLE vulnerability_occurrences (
project_id bigint NOT NULL,
scanner_id bigint NOT NULL,
primary_identifier_id bigint NOT NULL,
project_fingerprint bytea NOT NULL,
project_fingerprint bytea,
location_fingerprint bytea NOT NULL,
name character varying NOT NULL,
metadata_version character varying NOT NULL,

View File

@ -44,7 +44,8 @@ GET /projects/:id/feature_flags_user_lists
| `search` | string | no | Return user lists matching the search criteria. |
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/feature_flags_user_lists"
curl --header "PRIVATE-TOKEN: <your_access_token>" \
--url "https://gitlab.example.com/api/v4/projects/1/feature_flags_user_lists"
```
Example response:
@ -87,10 +88,11 @@ POST /projects/:id/feature_flags_user_lists
| `user_xids` | string | yes | A comma-separated list of external user IDs. |
```shell
curl "https://gitlab.example.com/api/v4/projects/1/feature_flags_user_lists" \
--header "PRIVATE-TOKEN: <your_access_token>" \
--header "Content-type: application/json" \
--data @- << EOF
curl --request POST \
--header "PRIVATE-TOKEN: <your_access_token>" \
--header "Content-type: application/json" \
--url "https://gitlab.example.com/api/v4/projects/1/feature_flags_user_lists" \
--data @- << EOF
{
"name": "my_user_list",
"user_xids": "user1,user2,user3"
@ -126,7 +128,8 @@ GET /projects/:id/feature_flags_user_lists/:iid
| `iid` | integer/string | yes | The internal ID of the project's feature flag user list. |
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/feature_flags_user_lists/1"
curl --header "PRIVATE-TOKEN: <your_access_token>" \
--url "https://gitlab.example.com/api/v4/projects/1/feature_flags_user_lists/1"
```
Example response:
@ -159,11 +162,11 @@ PUT /projects/:id/feature_flags_user_lists/:iid
| `user_xids` | string | no | A comma-separated list of external user IDs. |
```shell
curl "https://gitlab.example.com/api/v4/projects/1/feature_flags_user_lists/1" \
--header "PRIVATE-TOKEN: <your_access_token>" \
--header "Content-type: application/json" \
--request PUT \
--data @- << EOF
curl --request PUT \
--header "PRIVATE-TOKEN: <your_access_token>" \
--header "Content-type: application/json" \
--url "https://gitlab.example.com/api/v4/projects/1/feature_flags_user_lists/1" \
--data @- << EOF
{
"user_xids": "user2,user3,user4"
}
@ -198,5 +201,7 @@ DELETE /projects/:id/feature_flags_user_lists/:iid
| `iid` | integer/string | yes | The internal ID of the project's feature flag user list |
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" --request DELETE "https://gitlab.example.com/api/v4/projects/1/feature_flags_user_lists/1"
curl --request DELETE \
--header "PRIVATE-TOKEN: <your_access_token>" \
--url "https://gitlab.example.com/api/v4/projects/1/feature_flags_user_lists/1"
```

View File

@ -20974,14 +20974,28 @@ Requires ClickHouse. Premium and Ultimate with GitLab Duo Pro and Enterprise onl
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="aimetricscodecontributorscount"></a>`codeContributorsCount` | [`Int`](#int) | Number of code contributors. |
| <a id="aimetricscodesuggestionsacceptedcount"></a>`codeSuggestionsAcceptedCount` | [`Int`](#int) | Total count of code suggestions accepted by code contributors. |
| <a id="aimetricscodesuggestionscontributorscount"></a>`codeSuggestionsContributorsCount` | [`Int`](#int) | Number of code contributors who used GitLab Duo Code Suggestions features. |
| <a id="aimetricscodesuggestionsshowncount"></a>`codeSuggestionsShownCount` | [`Int`](#int) | Total count of code suggestions shown to code contributors. |
| <a id="aimetricscodesuggestionsacceptedcount"></a>`codeSuggestionsAcceptedCount` {{< icon name="warning-solid" >}} | [`Int`](#int) | **Deprecated** in GitLab 18.0. moved to codeSuggestions field. |
| <a id="aimetricscodesuggestionscontributorscount"></a>`codeSuggestionsContributorsCount` {{< icon name="warning-solid" >}} | [`Int`](#int) | **Deprecated** in GitLab 18.0. moved to codeSuggestions field. |
| <a id="aimetricscodesuggestionsshowncount"></a>`codeSuggestionsShownCount` {{< icon name="warning-solid" >}} | [`Int`](#int) | **Deprecated** in GitLab 18.0. moved to codeSuggestions field. |
| <a id="aimetricsduoassigneduserscount"></a>`duoAssignedUsersCount` | [`Int`](#int) | Total assigned Duo Pro and Enterprise seats. Ignores time period filter. Returns current data. |
| <a id="aimetricsduochatcontributorscount"></a>`duoChatContributorsCount` | [`Int`](#int) | Number of contributors who used GitLab Duo Chat features. |
| <a id="aimetricsduousedcount"></a>`duoUsedCount` | [`Int`](#int) | Number of contributors who used any GitLab Duo feature. |
| <a id="aimetricsrootcauseanalysisuserscount"></a>`rootCauseAnalysisUsersCount` | [`Int`](#int) | Number of users using troubleshoot within a failed pipeline. |
#### Fields with arguments
##### `AiMetrics.codeSuggestions`
Code suggestions metrics.
Returns [`codeSuggestionMetrics`](#codesuggestionmetrics).
###### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="aimetricscodesuggestionslanguages"></a>`languages` | [`[String!]`](#string) | Filter code suggestion metrics by one or more languages. |
### `AiSelfHostedModel`
Self-hosted LLM servers.
@ -34384,7 +34398,6 @@ Represents vulnerability finding of a security report on the pipeline.
| <a id="pipelinesecurityreportfindinglocation"></a>`location` | [`VulnerabilityLocation`](#vulnerabilitylocation) | Location metadata for the vulnerability. Its fields depend on the type of security scan that found the vulnerability. |
| <a id="pipelinesecurityreportfindingmergerequest"></a>`mergeRequest` | [`MergeRequest`](#mergerequest) | Merge request that fixes the vulnerability. |
| <a id="pipelinesecurityreportfindingproject"></a>`project` | [`Project`](#project) | Project on which the vulnerability finding was found. |
| <a id="pipelinesecurityreportfindingprojectfingerprint"></a>`projectFingerprint` {{< icon name="warning-solid" >}} | [`String`](#string) | **Deprecated** in GitLab 16.1. Use uuid instead. |
| <a id="pipelinesecurityreportfindingremediations"></a>`remediations` | [`[VulnerabilityRemediationType!]`](#vulnerabilityremediationtype) | Remediations of the security report finding. |
| <a id="pipelinesecurityreportfindingreporttype"></a>`reportType` | [`VulnerabilityReportType`](#vulnerabilityreporttype) | Type of the security report that found the vulnerability finding. |
| <a id="pipelinesecurityreportfindingscanner"></a>`scanner` | [`VulnerabilityScanner`](#vulnerabilityscanner) | Scanner metadata for the vulnerability. |
@ -41829,6 +41842,21 @@ X.509 signature for a signed commit.
| <a id="x509signatureverificationstatus"></a>`verificationStatus` | [`VerificationStatus`](#verificationstatus) | Indicates verification status of the associated key or certificate. |
| <a id="x509signaturex509certificate"></a>`x509Certificate` | [`X509Certificate`](#x509certificate) | Certificate used for the signature. |
### `codeSuggestionMetrics`
Requires ClickHouse. Premium and Ultimate with GitLab Duo Pro and Enterprise only.
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="codesuggestionmetricsacceptedcount"></a>`acceptedCount` | [`Int`](#int) | Total count of code suggestions accepted. |
| <a id="codesuggestionmetricsacceptedlinesofcode"></a>`acceptedLinesOfCode` | [`Int`](#int) | Sum of lines of code from code suggestions accepted. |
| <a id="codesuggestionmetricscontributorscount"></a>`contributorsCount` | [`Int`](#int) | Number of code contributors who used GitLab Duo Code Suggestions features. |
| <a id="codesuggestionmetricslanguages"></a>`languages` | [`[String!]`](#string) | List of languages with at least one suggestion shown or accepted. |
| <a id="codesuggestionmetricsshowncount"></a>`shownCount` | [`Int`](#int) | Total count of code suggestions shown. |
| <a id="codesuggestionmetricsshownlinesofcode"></a>`shownLinesOfCode` | [`Int`](#int) | Sum of lines of code from code suggestions shown. |
## Enumeration types
Also called _Enums_, enumeration types are a special kind of scalar that

View File

@ -83,7 +83,6 @@ Example response:
"metadata_version": "2.0",
"name": "Regular Expression Denial of Service in debug",
"primary_identifier_id": 135,
"project_fingerprint": "05e7cc9978ca495cf739a9f707ed34811e41c615",
"project_id": 24,
"raw_metadata": "{\"category\":\"dependency_scanning\",\"name\":\"Regular Expression Denial of Service\",\"message\":\"Regular Expression Denial of Service in debug\",\"description\":\"The debug module is vulnerable to regular expression denial of service when untrusted user input is passed into the `o` formatter. It takes around 50k characters to block for 2 seconds making this a low severity issue.\",\"cve\":\"yarn.lock:debug:gemnasium:37283ed4-0380-40d7-ada7-2d994afcc62a\",\"severity\":\"Unknown\",\"solution\":\"Upgrade to latest versions.\",\"scanner\":{\"id\":\"gemnasium\",\"name\":\"Gemnasium\"},\"location\":{\"file\":\"yarn.lock\",\"dependency\":{\"package\":{\"name\":\"debug\"},\"version\":\"1.0.5\"}},\"identifiers\":[{\"type\":\"gemnasium\",\"name\":\"Gemnasium-37283ed4-0380-40d7-ada7-2d994afcc62a\",\"value\":\"37283ed4-0380-40d7-ada7-2d994afcc62a\",\"url\":\"https://deps.sec.gitlab.com/packages/npm/debug/versions/1.0.5/advisories\"}],\"links\":[{\"url\":\"https://nodesecurity.io/advisories/534\"},{\"url\":\"https://github.com/visionmedia/debug/issues/501\"},{\"url\":\"https://github.com/visionmedia/debug/pull/504\"}],\"remediations\":[null]}",
"report_type": "dependency_scanning",
@ -166,7 +165,6 @@ Example response:
"metadata_version": "2.0",
"name": "Regular Expression Denial of Service in debug",
"primary_identifier_id": 135,
"project_fingerprint": "05e7cc9978ca495cf739a9f707ed34811e41c615",
"project_id": 24,
"raw_metadata": "{\"category\":\"dependency_scanning\",\"name\":\"Regular Expression Denial of Service\",\"message\":\"Regular Expression Denial of Service in debug\",\"description\":\"The debug module is vulnerable to regular expression denial of service when untrusted user input is passed into the `o` formatter. It takes around 50k characters to block for 2 seconds making this a low severity issue.\",\"cve\":\"yarn.lock:debug:gemnasium:37283ed4-0380-40d7-ada7-2d994afcc62a\",\"severity\":\"Unknown\",\"solution\":\"Upgrade to latest versions.\",\"scanner\":{\"id\":\"gemnasium\",\"name\":\"Gemnasium\"},\"location\":{\"file\":\"yarn.lock\",\"dependency\":{\"package\":{\"name\":\"debug\"},\"version\":\"1.0.5\"}},\"identifiers\":[{\"type\":\"gemnasium\",\"name\":\"Gemnasium-37283ed4-0380-40d7-ada7-2d994afcc62a\",\"value\":\"37283ed4-0380-40d7-ada7-2d994afcc62a\",\"url\":\"https://deps.sec.gitlab.com/packages/npm/debug/versions/1.0.5/advisories\"}],\"links\":[{\"url\":\"https://nodesecurity.io/advisories/534\"},{\"url\":\"https://github.com/visionmedia/debug/issues/501\"},{\"url\":\"https://github.com/visionmedia/debug/pull/504\"}],\"remediations\":[null]}",
"report_type": "dependency_scanning",

View File

@ -92,7 +92,6 @@ Example response:
"url": "https://brakemanscanner.org/docs/warning_types/command_injection/"
}
],
"project_fingerprint": "ac218b1770af030cfeef967752ab803c55afb36d",
"uuid": "ad5e3be3-a193-55f5-a200-bc12865fb09c",
"create_jira_issue_url": null,
"false_positive": true,

View File

@ -5346,7 +5346,7 @@ In this example, GitLab launches two containers for the job:
- A Ruby container that runs the `script` commands.
- A PostgreSQL container. The `script` commands in the Ruby container can connect to
the PostgreSQL database at the `db-postgrest` hostname.
the PostgreSQL database at the `db-postgres` hostname.
**Related topics**:

View File

@ -269,7 +269,7 @@ positives.
| `ADDITIONAL_CA_CERT_BUNDLE` | `""` | Bundle of CA certs that you want to trust. See [Using a custom SSL CA certificate authority](#using-a-custom-ssl-ca-certificate-authority) for more details. |
| `CI_APPLICATION_REPOSITORY` | `$CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG` | Docker repository URL for the image to be scanned. |
| `CI_APPLICATION_TAG` | `$CI_COMMIT_SHA` | Docker repository tag for the image to be scanned. |
| `CS_ANALYZER_IMAGE` | `registry.gitlab.com/security-products/container-scanning:7` | Docker image of the analyzer. Do not use the `:latest` tag with analyzer images provided by GitLab. |
| `CS_ANALYZER_IMAGE` | `registry.gitlab.com/security-products/container-scanning:8` | Docker image of the analyzer. Do not use the `:latest` tag with analyzer images provided by GitLab. |
| `CS_DEFAULT_BRANCH_IMAGE` | `""` | The name of the `CS_IMAGE` on the default branch. See [Setting the default branch image](#setting-the-default-branch-image) for more details. |
| `CS_DISABLE_DEPENDENCY_LIST` | `"false"` | {{< icon name="warning" >}} **[Removed](https://gitlab.com/gitlab-org/gitlab/-/issues/439782)** in GitLab 17.0. |
| `CS_DISABLE_LANGUAGE_VULNERABILITY_SCAN` | `"true"` | Disable scanning for language-specific packages installed in the scanned image. |
@ -548,8 +548,8 @@ For container scanning, import the following images from `registry.gitlab.com` i
[local Docker container registry](../../packages/container_registry/_index.md):
```plaintext
registry.gitlab.com/security-products/container-scanning:7
registry.gitlab.com/security-products/container-scanning/trivy:7
registry.gitlab.com/security-products/container-scanning:8
registry.gitlab.com/security-products/container-scanning/trivy:8
```
The process for importing Docker images into a local offline Docker registry depends on
@ -589,7 +589,7 @@ following `.gitlab-ci.yml` example as a template.
```yaml
variables:
SOURCE_IMAGE: registry.gitlab.com/security-products/container-scanning:7
SOURCE_IMAGE: registry.gitlab.com/security-products/container-scanning:8
TARGET_IMAGE: $CI_REGISTRY/namespace/container-scanning
image: docker:latest

View File

@ -1169,12 +1169,12 @@ To use dependency scanning with all [supported languages and frameworks](#suppor
your [local Docker container registry](../../packages/container_registry/_index.md):
```plaintext
registry.gitlab.com/security-products/gemnasium:5
registry.gitlab.com/security-products/gemnasium:5-fips
registry.gitlab.com/security-products/gemnasium-maven:5
registry.gitlab.com/security-products/gemnasium-maven:5-fips
registry.gitlab.com/security-products/gemnasium-python:5
registry.gitlab.com/security-products/gemnasium-python:5-fips
registry.gitlab.com/security-products/gemnasium:6
registry.gitlab.com/security-products/gemnasium:6-fips
registry.gitlab.com/security-products/gemnasium-maven:6
registry.gitlab.com/security-products/gemnasium-maven:6-fips
registry.gitlab.com/security-products/gemnasium-python:6
registry.gitlab.com/security-products/gemnasium-python:6-fips
```
The process for importing Docker images into a local offline Docker registry depends on

View File

@ -756,7 +756,7 @@ predefined project variable, not defined in the project's `.gitlab-ci.yml` file.
```yaml
variables:
CS_ANALYZER_IMAGE: "$CI_TEMPLATE_REGISTRY_HOST/security-products/container-scanning:7"
CS_ANALYZER_IMAGE: "$CI_TEMPLATE_REGISTRY_HOST/security-products/container-scanning:8"
CS_IMAGE: alpine:latest
policy::container-security:

View File

@ -17,6 +17,10 @@ module ActiveContext
fields << Field::Keyword.new(name, index: true)
end
def text(name)
fields << Field::Text.new(name, index: false)
end
def vector(name, dimensions:, index: true)
fields << Field::Vector.new(name, dimensions: dimensions, index: index)
end
@ -32,6 +36,7 @@ module ActiveContext
class Bigint < Field; end
class Keyword < Field; end
class Text < Field; end
class Vector < Field; end
end
end

View File

@ -66,6 +66,8 @@ module ActiveContext
{ type: 'long' }
when Field::Keyword
{ type: 'keyword' }
when Field::Text
{ type: 'text' }
when Field::Vector
vector_field_mapping(field)
else

View File

@ -59,6 +59,7 @@ module ActiveContext
# @raise [ArgumentError] If the query type is not supported
def process(node)
case node.type
when :all then process_all
when :filter then process_filter(node.value)
when :prefix then process_prefix(node.value)
when :or then process_or(node)
@ -72,6 +73,10 @@ module ActiveContext
private
def process_all
{ query: { match_all: {} } }
end
# Processes filter conditions into term or terms queries
#
# @param conditions [Hash] The filter conditions where keys are fields and values are the terms

View File

@ -75,7 +75,7 @@ module ActiveContext
when Field::Bigint
# Bigint is 8 bytes
fixed_columns << [field, 8]
when Field::Keyword
when Field::Keyword, Field::Text
# Text fields are variable width
variable_columns << field
else
@ -93,7 +93,7 @@ module ActiveContext
table.column(field.name, "vector(#{field.options[:dimensions]})")
when Field::Bigint
table.bigint(field.name, **field.options.except(:index))
when Field::Keyword
when Field::Keyword, Field::Text
table.text(field.name, **field.options.except(:index))
else
raise ArgumentError, "Unknown field type: #{field.class}"

View File

@ -22,6 +22,7 @@ module ActiveContext
# Processes a query node and returns the corresponding ActiveRecord relation
def process(node)
case node.type
when :all then process_all
when :filter then process_filter(node.value)
when :prefix then process_prefix(node.value)
when :and then process_and(node.children)
@ -37,6 +38,10 @@ module ActiveContext
attr_reader :model, :base_relation
def process_all
base_relation
end
def process_filter(conditions)
relation = base_relation
conditions.each do |key, value|

View File

@ -26,6 +26,7 @@
# ActiveContext::Query.knn(target: "similarity", vector: [0.1, 0.2, 0.3], limit: 5)
#
# Supported Query Types:
# - :all - Return all documents
# - :filter - Exact match conditions
# - :prefix - Prefix/starts-with conditions
# - :limit - Restricts number of results
@ -44,11 +45,15 @@
module ActiveContext
class Query
ALLOWED_TYPES = [:filter, :prefix, :limit, :knn, :and, :or].freeze
ALLOWED_TYPES = [:all, :filter, :prefix, :limit, :knn, :and, :or].freeze
SPACES_PER_INDENT = 2
class << self
# Class methods to start the chain
def all
new(type: :all)
end
def filter(**conditions)
raise ArgumentError, "Filter cannot be empty" if conditions.empty?

View File

@ -10,6 +10,7 @@ RSpec.describe ActiveContext::Databases::Elasticsearch::Processor do
let(:simple_filter) { ActiveContext::Query.filter(status: 'active') }
let(:simple_prefix) { ActiveContext::Query.prefix(name: 'test') }
let(:simple_all) { ActiveContext::Query.all }
let(:simple_knn) do
ActiveContext::Query.knn(
target: 'embedding',
@ -379,5 +380,15 @@ RSpec.describe ActiveContext::Databases::Elasticsearch::Processor do
)
end
end
context 'with all queries' do
it 'creates a match_all query' do
result = processor.process(simple_all)
expect(result).to eq(
query: { match_all: {} }
)
end
end
end
end

View File

@ -10,6 +10,7 @@ RSpec.describe ActiveContext::Databases::Opensearch::Processor do
let(:simple_filter) { ActiveContext::Query.filter(status: 'active') }
let(:simple_prefix) { ActiveContext::Query.prefix(name: 'test') }
let(:simple_all) { ActiveContext::Query.all }
let(:simple_knn) do
ActiveContext::Query.knn(
target: 'embedding',
@ -369,5 +370,15 @@ RSpec.describe ActiveContext::Databases::Opensearch::Processor do
)
end
end
context 'with all queries' do
it 'creates a match_all query' do
result = processor.process(simple_all)
expect(result).to eq(
query: { match_all: {} }
)
end
end
end
end

View File

@ -182,4 +182,10 @@ RSpec.describe ActiveContext::Databases::Postgresql::Processor, feature_category
"SELECT subq.* FROM (SELECT \"items\".* FROM \"items\" " \
"ORDER BY \"embedding\" <=> '[0.1,0.2]' LIMIT 5) subq LIMIT 10"
end
context 'with all queries' do
it_behaves_like 'a SQL transformer',
ActiveContext::Query.all,
"SELECT \"items\".* FROM \"items\""
end
end

View File

@ -2,6 +2,15 @@
RSpec.describe ActiveContext::Query do
describe 'class methods' do
describe '.all' do
it 'creates an all query' do
query = described_class.all
expect(query.type).to eq(:all)
expect(query.value).to be_nil
expect(query.children).to be_empty
end
end
describe '.filter' do
it 'creates a filter query with valid conditions' do
query = described_class.filter(project_id: 1)
@ -172,6 +181,18 @@ RSpec.describe ActiveContext::Query do
end
describe '#inspect_ast' do
it 'generates a readable AST representation for a simple all query' do
query = described_class.all
ast = query.inspect_ast
expect(ast).to eq('all')
end
it 'generates a readable AST representation for an all query with limit' do
query = described_class.all.limit(10)
ast = query.inspect_ast
expect(ast).to eq("limit(10)\n all")
end
it 'generates a readable AST representation for a simple filter query' do
query = described_class.filter(project_id: 1)
ast = query.inspect_ast
@ -215,7 +236,7 @@ RSpec.describe ActiveContext::Query do
it 'raises an error for invalid query type' do
expect { described_class.new(type: :invalid) }.to raise_error(
ArgumentError,
/Invalid type: invalid\. Allowed types are: filter, prefix, limit, knn, and, or/
/Invalid type: invalid\. Allowed types are: all, filter, prefix, limit, knn, and, or/
)
end
end

View File

@ -727,7 +727,8 @@ module API
# @content_disposition controls the Content-Disposition response header. nil by default. Forced to attachment for object storage disabled mode.
# @content_type controls the Content-Type response header. By default, it will rely on the 'application/octet-stream' value or the content type detected by carrierwave.
# @extra_response_headers. Set additional response headers. Not used in the direct download supported case.
def present_carrierwave_file!(file, supports_direct_download: true, content_disposition: nil, content_type: nil, extra_response_headers: {})
# @extra_send_url_params. Additional parameters to send to workhorse send_url call. See Gitlab::Workhorse.send_url for more information
def present_carrierwave_file!(file, supports_direct_download: true, content_disposition: nil, content_type: nil, extra_response_headers: {}, extra_send_url_params: {})
return not_found! unless file&.exists?
if content_disposition
@ -749,7 +750,7 @@ module API
else
response_headers = extra_response_headers.merge('Content-Type' => content_type, 'Content-Disposition' => response_disposition).compact_blank
header(*Gitlab::Workhorse.send_url(file.url, response_headers: response_headers))
header(*Gitlab::Workhorse.send_url(file.url, response_headers: response_headers, **extra_send_url_params))
status :ok
body '' # to avoid an error from API::APIGuard::ResponseCoercerMiddleware
end

View File

@ -54,17 +54,18 @@ module API
nil
end
def download_package_file!(package_file)
def download_package_file!(package_file, extra_send_url_params: {}, extra_response_headers: {})
package_file.package.touch_last_downloaded_at
file = package_file.file
extra_response_headers = { SHA1_CHECKSUM_HEADER => package_file.file_sha1 }
extra_response_headers[MD5_CHECKSUM_HEADER] = package_file.file_md5 unless Gitlab::FIPS.enabled?
headers = extra_response_headers.merge(SHA1_CHECKSUM_HEADER => package_file.file_sha1)
headers[MD5_CHECKSUM_HEADER] = package_file.file_md5 unless Gitlab::FIPS.enabled?
present_carrierwave_file!(
file,
supports_direct_download: false, # we can't support direct download if we have custom response headers
extra_response_headers: extra_response_headers
extra_response_headers: headers,
extra_send_url_params: extra_send_url_params
)
end
end

View File

@ -2,25 +2,23 @@
module Banzai
module Filter
# Filter which looks for possible paragraphs with quick action lines, and allows
# another processor to do final determination. Paragraph source position
# Filter which extracts top-level paragraph sourcepos, so
# another processor can determine if it's a quick action. Paragraph source position
# is returned in `result[:quick_action_paragraphs]`.
class QuickActionFilter < HTML::Pipeline::Filter
def call
result[:quick_action_paragraphs] = []
doc.children.xpath('self::p').each do |node|
# don't use `xpath` as it can take too long
doc.children.each do |node|
next unless node.name == 'p'
next unless node.attributes['data-sourcepos']
next unless %r{^/}.match?(node.content)
sourcepos = ::Banzai::Filter::MarkdownFilter.parse_sourcepos(node.attributes['data-sourcepos'].value)
node.children.xpath('self::text()').each do |text_node|
next unless %r{^/}.match?(text_node.content)
result[:quick_action_paragraphs] <<
{ start_line: sourcepos[:start][:row], end_line: sourcepos[:end][:row] }
break
end
result[:quick_action_paragraphs] <<
{ start_line: sourcepos[:start][:row], end_line: sourcepos[:end][:row] }
end
doc

View File

@ -18,7 +18,12 @@ module Banzai
<p>Timeout while sanitizing links - rendering aborted. Please reduce the number of links if possible.</p>
HTML
# We have seen documents with thousands of links
MAX_LINK_ATTRIBUTES = 10000
def call
raise Timeout::Error if doc.xpath(self.class::XPATH).count > MAX_LINK_ATTRIBUTES
doc.xpath(self.class::XPATH).each do |el|
sanitize_unsafe_links({ node: el })
end

View File

@ -16,7 +16,6 @@ module Gitlab
attr_reader :metadata_version
attr_reader :name
attr_reader :old_location
attr_reader :project_fingerprint
attr_reader :report_type
attr_reader :scanner
attr_reader :scan
@ -55,8 +54,6 @@ module Gitlab
@vulnerability_finding_signatures_enabled = vulnerability_finding_signatures_enabled
@found_by_pipeline = found_by_pipeline
@cvss = cvss
@project_fingerprint = generate_project_fingerprint
end
def to_hash
@ -69,7 +66,6 @@ module Gitlab
evidence
metadata_version
name
project_fingerprint
raw_metadata
report_type
scanner
@ -193,10 +189,6 @@ module Gitlab
private
def generate_project_fingerprint
Digest::SHA1.hexdigest(uuid.to_s)
end
def location_fingerprints
@location_fingerprints ||= signature_hexes << location&.fingerprint
end

View File

@ -22,7 +22,7 @@
# 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:7"
CS_ANALYZER_IMAGE: "$CI_TEMPLATE_REGISTRY_HOST/security-products/container-scanning:8"
CS_SCHEMA_MODEL: 15
container_scanning:

View File

@ -30,7 +30,7 @@ variables:
# (SAST, Dependency Scanning, ...)
AST_ENABLE_MR_PIPELINES: "true"
#
CS_ANALYZER_IMAGE: "$CI_TEMPLATE_REGISTRY_HOST/security-products/container-scanning:7"
CS_ANALYZER_IMAGE: "$CI_TEMPLATE_REGISTRY_HOST/security-products/container-scanning:8"
CS_SCHEMA_MODEL: 15
# Provide a base job for extensibility until delivery of https://gitlab.com/gitlab-org/gitlab/-/issues/215470

View File

@ -15,7 +15,7 @@ variables:
#
DS_EXCLUDED_ANALYZERS: ""
DS_EXCLUDED_PATHS: "spec, test, tests, tmp"
DS_MAJOR_VERSION: 5
DS_MAJOR_VERSION: 6
DS_SCHEMA_MODEL: 15
dependency_scanning:

View File

@ -20,7 +20,7 @@ variables:
#
DS_EXCLUDED_ANALYZERS: ""
DS_EXCLUDED_PATHS: "spec, test, tests, tmp"
DS_MAJOR_VERSION: 5
DS_MAJOR_VERSION: 6
DS_SCHEMA_MODEL: 15
# Use this variable to enforce the new Dependency Scanning analyzer for all projects
DS_ENFORCE_NEW_ANALYZER: 'false'

View File

@ -183,21 +183,21 @@ kics:
gemnasium:
extends: .download_images
variables:
SECURE_BINARIES_ANALYZER_VERSION: "5"
SECURE_BINARIES_ANALYZER_VERSION: "6"
rules:
- if: '$SECURE_BINARIES_DOWNLOAD_IMAGES == "true" && $SECURE_BINARIES_ANALYZERS =~ /\bgemnasium\b/'
gemnasium-maven:
extends: .download_images
variables:
SECURE_BINARIES_ANALYZER_VERSION: "5"
SECURE_BINARIES_ANALYZER_VERSION: "6"
rules:
- if: '$SECURE_BINARIES_DOWNLOAD_IMAGES == "true" && $SECURE_BINARIES_ANALYZERS =~ /\bgemnasium-maven\b/'
gemnasium-python:
extends: .download_images
variables:
SECURE_BINARIES_ANALYZER_VERSION: "5"
SECURE_BINARIES_ANALYZER_VERSION: "6"
rules:
- if: '$SECURE_BINARIES_DOWNLOAD_IMAGES == "true" && $SECURE_BINARIES_ANALYZERS =~ /\bgemnasium-python\b/'

View File

@ -3,14 +3,15 @@
module Gitlab
module ImportExport
class MembersMapper
def initialize(exported_members:, user:, importable:)
def initialize(exported_members:, user:, importable:, default_member: true)
@exported_members = user.admin? ? exported_members : []
@user = user
@importable = importable
# This needs to run first, as second call would be from #map
# which means Project/Group members already exist.
ensure_default_member!
# Skip this when importing single relations to avoid destroying project members
ensure_default_member! if default_member
end
def map

View File

@ -1318,7 +1318,6 @@ ee:
- :report_type
- :findings
findings:
- :project_fingerprint
- :project_id
- :location_fingerprint
- :primary_identifier_fingerprint
@ -1343,7 +1342,6 @@ ee:
primary_identifier: *identifiers_definition
vulnerability_finding:
- :uuid
- :project_fingerprint
- :project_id
- :location_fingerprint
- :name

View File

@ -0,0 +1,22 @@
# frozen_string_literal: true
module Gitlab
module Middleware
class SecureHeaders
def initialize(app)
@app = app
end
def call(env)
status, headers, body = @app.call(env)
# Remove NEL policy from the policy cache by setting max_age to 0.
# https://w3c.github.io/network-error-logging/#the-max_age-member
# https://w3c.github.io/network-error-logging/#example-2
headers['NEL'] = '{"max_age": 0}'
[status, headers, body]
end
end
end
end

View File

@ -187,7 +187,8 @@ module Gitlab
timeouts: {},
response_statuses: {},
response_headers: {},
allowed_uris: []
allowed_uris: [],
restrict_forwarded_response_headers: {}
)
params = {
'URL' => url,
@ -199,7 +200,7 @@ module Gitlab
'Header' => headers.transform_values { |v| Array.wrap(v) },
'ResponseHeaders' => response_headers.transform_values { |v| Array.wrap(v) },
'Method' => method
}.compact
}.merge(restrict_forwarded_response_headers_params(restrict_forwarded_response_headers)).compact
if timeouts.present?
params['DialTimeout'] = "#{timeouts[:open]}s" if timeouts[:open]
@ -243,7 +244,8 @@ module Gitlab
upload_config: {},
response_headers: {},
ssrf_filter: false,
allowed_uris: [])
allowed_uris: [],
restrict_forwarded_response_headers: {})
params = {
'AllowLocalhost' => allow_localhost,
'AllowedURIs' => allowed_uris,
@ -257,9 +259,8 @@ module Gitlab
'Headers' => (upload_config[:headers] || {}).transform_values { |v| Array.wrap(v) },
'AuthorizedUploadResponse' => upload_config[:authorized_upload_response] || {}
}.compact_blank!
}
}.merge(restrict_forwarded_response_headers_params(restrict_forwarded_response_headers))
params.compact_blank!
[
SEND_DATA_HEADER,
"send-dependency:#{encode(params)}"
@ -399,6 +400,16 @@ module Gitlab
)
}
end
def restrict_forwarded_response_headers_params(params)
params[:enabled] = false unless params.key?(:enabled)
{
'RestrictForwardedResponseHeaders' => {
'Enabled' => params[:enabled],
'AllowList' => params[:allow_list] || []
}
}.compact_blank!
end
end
end
end

View File

@ -59,7 +59,7 @@
"@floating-ui/dom": "^1.2.9",
"@gitlab/application-sdk-browser": "^0.3.4",
"@gitlab/at.js": "1.5.7",
"@gitlab/cluster-client": "^2.5.0",
"@gitlab/cluster-client": "^3.0.0",
"@gitlab/duo-ui": "^8.10.0",
"@gitlab/favicon-overlay": "2.0.0",
"@gitlab/fonts": "^1.3.0",
@ -305,7 +305,7 @@
"swagger-cli": "^4.0.4",
"tailwindcss": "^3.4.1",
"timezone-mock": "^1.0.8",
"vite": "^6.3.1",
"vite": "^6.3.2",
"vite-plugin-ruby": "^5.1.1",
"vue-loader-vue3": "npm:vue-loader@17.4.2",
"vue-test-utils-compat": "0.0.14",

View File

@ -153,6 +153,21 @@ RSpec.describe Settings, feature_category: :system_access do
allow(Gitlab::Application.credentials)
.to receive(:db_key_base)
.and_return(raw_keys)
# Reset memoization
described_class.instance_variable_set(:@db_key_base_keys, nil)
end
describe 'memoization' do
let(:raw_keys) { 'a' }
it 'memoizes the value' do
db_key_base_keys = described_class.db_key_base_keys
expect(described_class.db_key_base_keys).to be(db_key_base_keys)
expect(Gitlab::Application.credentials)
.to have_received(:db_key_base).once
end
end
context 'when db key base secret is a string' do

View File

@ -1,3 +1,3 @@
{"project_id":5,"author_id":1,"title":"Regular expression with non-literal value","description":null,"severity":"medium","report_type":"sast","vulnerability_finding":{"severity":"medium","report_type":"sast","project_id":5,"project_fingerprint":"4ce7494840bb1882d5a9003b0f272f8e3e22c7a5","location_fingerprint":"4f7a2fffbb791c4cc8d1454db40b80f7fa9ed5be","name":"Regular expression with non-literal value","metadata_version":"15.1.4","raw_metadata":"{\"id\":\"b13b66b99eabefb8bc0d385b90cb952734e246ff3477a8ee563d6d04ef4bded4\",\"category\":\"sast\",\"name\":\"Regular expression with non-literal value\",\"description\":\"The `RegExp` constructor was called with a non-literal value. If an adversary were able to\\nsupply a malicious regex, they could cause a Regular Expression Denial of Service (ReDoS)\\nagainst the application. In Node applications, this could cause the entire application to no\\nlonger be responsive to other users' requests.\\n\\nTo remediate this issue, never allow user-supplied regular expressions. Instead, the regular \\nexpression should be hardcoded. If this is not possible, consider using an alternative regular\\nexpression engine such as [node-re2](https://www.npmjs.com/package/re2). RE2 is a safe alternative \\nthat does not support backtracking, which is what leads to ReDoS.\\n\\nExample using re2 which does not support backtracking (Note: it is still recommended to\\nnever use user-supplied input):\\n```\\n// Import the re2 module\\nconst RE2 = require('re2');\\n\\nfunction match(userSuppliedRegex, userInput) {\\n // Create a RE2 object with the user supplied regex, this is relatively safe\\n // due to RE2 not supporting backtracking which can be abused to cause long running\\n // queries\\n var re = new RE2(userSuppliedRegex);\\n // Execute the regular expression against some userInput\\n var result = re.exec(userInput);\\n // Work with the result\\n}\\n```\\n\\nFor more information on Regular Expression DoS see:\\n- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS\\n\",\"cve\":\"semgrep_id:eslint.detect-non-literal-regexp:515:515\",\"severity\":\"Medium\",\"scanner\":{\"id\":\"semgrep\",\"name\":\"Semgrep\"},\"location\":{\"file\":\"common/static/ace/ext-language_tools.js\",\"start_line\":515},\"identifiers\":[{\"type\":\"semgrep_id\",\"name\":\"eslint.detect-non-literal-regexp\",\"value\":\"eslint.detect-non-literal-regexp\",\"url\":\"https://semgrep.dev/r/gitlab.eslint.detect-non-literal-regexp\"},{\"type\":\"cwe\",\"name\":\"CWE-185\",\"value\":\"185\",\"url\":\"https://cwe.mitre.org/data/definitions/185.html\"},{\"type\":\"owasp\",\"name\":\"A03:2021 - Injection\",\"value\":\"A03:2021\"},{\"type\":\"owasp\",\"name\":\"A1:2017 - Injection\",\"value\":\"A1:2017\"},{\"type\":\"eslint_rule_id\",\"name\":\"ESLint rule ID/detect-non-literal-regexp\",\"value\":\"detect-non-literal-regexp\"}],\"tracking\":{\"type\":\"source\",\"items\":[{\"file\":\"common/static/ace/ext-language_tools.js\",\"line_start\":515,\"line_end\":515,\"signatures\":[{\"algorithm\":\"scope_offset\",\"value\":\"common/static/ace/ext-language_tools.js|func[0]:498\"}]}]}}","detection_method":"gitlab_security_report","uuid":"fa74cb01-2544-5d42-b9e8-0150119bf6cb","scanner":{"project_id":5,"external_id":"semgrep","name":"Semgrep","vendor":"GitLab"},"primary_identifier":{"project_id":5,"fingerprint":"a751f35f1185de7ca5e6c0610c3bca21eb25ac9a","external_type":"semgrep_id","external_id":"eslint.detect-non-literal-regexp","name":"eslint.detect-non-literal-regexp","url":"https://semgrep.dev/r/gitlab.eslint.detect-non-literal-regexp"},"identifiers":[{"project_id":5,"fingerprint":"08de3511f2132da4d24f1b8b1d3ca14368a0259b","external_type":"owasp","external_id":"A1:2017","name":"A1:2017 - Injection","url":null},{"project_id":5,"fingerprint":"7153fe286fd77c7a6250aa9603b82d44ab1c31e4","external_type":"cwe","external_id":"185","name":"CWE-185","url":"https://cwe.mitre.org/data/definitions/185.html"},{"project_id":5,"fingerprint":"a15f44ab746431d58b21b4fc67d8c4d3fb160ca0","external_type":"eslint_rule_id","external_id":"detect-non-literal-regexp","name":"ESLint rule ID/detect-non-literal-regexp","url":null},{"project_id":5,"fingerprint":"a751f35f1185de7ca5e6c0610c3bca21eb25ac9a","external_type":"semgrep_id","external_id":"eslint.detect-non-literal-regexp","name":"eslint.detect-non-literal-regexp","url":"https://semgrep.dev/r/gitlab.eslint.detect-non-literal-regexp"},{"project_id":5,"fingerprint":"a8e828eea3aba35916401da9304619f0a218119b","external_type":"owasp","external_id":"A03:2021","name":"A03:2021 - Injection","url":null}],"initial_finding_pipeline":{"iid":438},"latest_finding_pipeline":{"iid":438}},"vulnerability_read":{"project_id":5,"scanner":{"project_id":5,"external_id":"semgrep","name":"Semgrep","vendor":"GitLab"}}}
{"project_id":5,"author_id":1,"title":"Incorrect regular expression","description":null,"severity":"medium","report_type":"sast","vulnerability_finding":{"severity":"medium","report_type":"sast","project_id":5,"project_fingerprint":"46e1dffeb673fa9e3de7343653b84dd9826e7312","location_fingerprint":"f866afbfc47ac0fae0da7c6df8e5ed35330e4384","name":"Incorrect regular expression","metadata_version":"15.1.4","raw_metadata":"{\"id\":\"0152dfdd49aa1b9636cd267c12d080250199f15f21f427d3bed1a07a002e011f\",\"category\":\"sast\",\"name\":\"Incorrect regular expression\",\"description\":\"Ensure that the regex used to compare with user supplied input is safe from regular expression denial of service.\\n\",\"cve\":\"semgrep_id:nodejs_scan.javascript-dos-rule-regex_dos:1050:1052\",\"severity\":\"Medium\",\"scanner\":{\"id\":\"semgrep\",\"name\":\"Semgrep\"},\"location\":{\"file\":\"themis/static/assets/plugins/jquery-ui/ui/jquery.ui.sortable.js\",\"start_line\":1050,\"end_line\":1052},\"identifiers\":[{\"type\":\"semgrep_id\",\"name\":\"nodejs_scan.javascript-dos-rule-regex_dos\",\"value\":\"nodejs_scan.javascript-dos-rule-regex_dos\"},{\"type\":\"cwe\",\"name\":\"CWE-185\",\"value\":\"185\",\"url\":\"https://cwe.mitre.org/data/definitions/185.html\"},{\"type\":\"owasp\",\"name\":\"A05:2021 - Security Misconfiguration\",\"value\":\"A05:2021\"},{\"type\":\"owasp\",\"name\":\"A6:2017 - Security Misconfiguration\",\"value\":\"A6:2017\"},{\"type\":\"njsscan_rule_type\",\"name\":\"NodeJS Scan ID javascript-dos-rule-regex_dos\",\"value\":\"Ensure that the regex used to compare with user supplied input is safe from regular expression denial of service.\"}],\"tracking\":{\"type\":\"source\",\"items\":[{\"file\":\"themis/static/assets/plugins/jquery-ui/ui/jquery.ui.sortable.js\",\"line_start\":1050,\"line_end\":1050,\"signatures\":[{\"algorithm\":\"scope_offset\",\"value\":\"themis/static/assets/plugins/jquery-ui/ui/jquery.ui.sortable.js|func($, undefined)[0]:1034\"}]}]}}","detection_method":"gitlab_security_report","uuid":"fa2589df-c1ad-5108-93f0-90237b17c1b1","scanner":{"project_id":5,"external_id":"semgrep","name":"Semgrep","vendor":"GitLab"},"primary_identifier":{"project_id":5,"fingerprint":"ad9e1d2b073e1c296088e8fbedf8ed738d06f88a","external_type":"semgrep_id","external_id":"nodejs_scan.javascript-dos-rule-regex_dos","name":"nodejs_scan.javascript-dos-rule-regex_dos","url":null},"identifiers":[{"project_id":5,"fingerprint":"2bd02e525f0e78f8745e5a063ca1b5f396527a41","external_type":"owasp","external_id":"A6:2017","name":"A6:2017 - Security Misconfiguration","url":null},{"project_id":5,"fingerprint":"3f2c4e94cf8c0b53c44cb5b187963b753da9e882","external_type":"owasp","external_id":"A05:2021","name":"A05:2021 - Security Misconfiguration","url":null},{"project_id":5,"fingerprint":"518290ee3e47f4a5bba33213ca8a82e4c0d8697d","external_type":"njsscan_rule_type","external_id":"Ensure that the regex used to compare with user supplied input is safe from regular expression denial of service.","name":"NodeJS Scan ID javascript-dos-rule-regex_dos","url":null},{"project_id":5,"fingerprint":"7153fe286fd77c7a6250aa9603b82d44ab1c31e4","external_type":"cwe","external_id":"185","name":"CWE-185","url":"https://cwe.mitre.org/data/definitions/185.html"},{"project_id":5,"fingerprint":"ad9e1d2b073e1c296088e8fbedf8ed738d06f88a","external_type":"semgrep_id","external_id":"nodejs_scan.javascript-dos-rule-regex_dos","name":"nodejs_scan.javascript-dos-rule-regex_dos","url":null}],"initial_finding_pipeline":{"iid":438},"latest_finding_pipeline":{"iid":438}},"vulnerability_read":{"project_id":5,"scanner":{"project_id":5,"external_id":"semgrep","name":"Semgrep","vendor":"GitLab"}}}
{"project_id":5,"author_id":1,"title":"Regular expression with non-literal value","description":null,"severity":"medium","report_type":"sast","vulnerability_finding":{"severity":"medium","report_type":"sast","project_id":5,"project_fingerprint":"ea561c323d8e5e87040ad59ca2b926f2b005255c","location_fingerprint":"708aa3150b2b448e6894dd447689336d0ce63f19","name":"Regular expression with non-literal value","metadata_version":"15.1.4","raw_metadata":"{\"id\":\"f8c645cd515f94924c9a8fe73cc3e2bcf08b90ee9936462b6da57b6c28b52803\",\"category\":\"sast\",\"name\":\"Regular expression with non-literal value\",\"description\":\"The `RegExp` constructor was called with a non-literal value. If an adversary were able to\\nsupply a malicious regex, they could cause a Regular Expression Denial of Service (ReDoS)\\nagainst the application. In Node applications, this could cause the entire application to no\\nlonger be responsive to other users' requests.\\n\\nTo remediate this issue, never allow user-supplied regular expressions. Instead, the regular \\nexpression should be hardcoded. If this is not possible, consider using an alternative regular\\nexpression engine such as [node-re2](https://www.npmjs.com/package/re2). RE2 is a safe alternative \\nthat does not support backtracking, which is what leads to ReDoS.\\n\\nExample using re2 which does not support backtracking (Note: it is still recommended to\\nnever use user-supplied input):\\n```\\n// Import the re2 module\\nconst RE2 = require('re2');\\n\\nfunction match(userSuppliedRegex, userInput) {\\n // Create a RE2 object with the user supplied regex, this is relatively safe\\n // due to RE2 not supporting backtracking which can be abused to cause long running\\n // queries\\n var re = new RE2(userSuppliedRegex);\\n // Execute the regular expression against some userInput\\n var result = re.exec(userInput);\\n // Work with the result\\n}\\n```\\n\\nFor more information on Regular Expression DoS see:\\n- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS\\n\",\"cve\":\"semgrep_id:eslint.detect-non-literal-regexp:1108:1108\",\"severity\":\"Medium\",\"scanner\":{\"id\":\"semgrep\",\"name\":\"Semgrep\"},\"location\":{\"file\":\"themis/static/assets/plugins/jquery-ui/ui/jquery.ui.datepicker.js\",\"start_line\":1108},\"identifiers\":[{\"type\":\"semgrep_id\",\"name\":\"eslint.detect-non-literal-regexp\",\"value\":\"eslint.detect-non-literal-regexp\",\"url\":\"https://semgrep.dev/r/gitlab.eslint.detect-non-literal-regexp\"},{\"type\":\"cwe\",\"name\":\"CWE-185\",\"value\":\"185\",\"url\":\"https://cwe.mitre.org/data/definitions/185.html\"},{\"type\":\"owasp\",\"name\":\"A03:2021 - Injection\",\"value\":\"A03:2021\"},{\"type\":\"owasp\",\"name\":\"A1:2017 - Injection\",\"value\":\"A1:2017\"},{\"type\":\"eslint_rule_id\",\"name\":\"ESLint rule ID/detect-non-literal-regexp\",\"value\":\"detect-non-literal-regexp\"}],\"tracking\":{\"type\":\"source\",\"items\":[{\"file\":\"themis/static/assets/plugins/jquery-ui/ui/jquery.ui.datepicker.js\",\"line_start\":1108,\"line_end\":1108,\"signatures\":[{\"algorithm\":\"scope_offset\",\"value\":\"themis/static/assets/plugins/jquery-ui/ui/jquery.ui.datepicker.js|func($, undefined)[0]|getNumber[0]:4\"}]}]}}","detection_method":"gitlab_security_report","uuid":"f72b22e4-1e01-5c53-95a5-5e3a1e2f2b16","scanner":{"project_id":5,"external_id":"semgrep","name":"Semgrep","vendor":"GitLab"},"primary_identifier":{"project_id":5,"fingerprint":"a751f35f1185de7ca5e6c0610c3bca21eb25ac9a","external_type":"semgrep_id","external_id":"eslint.detect-non-literal-regexp","name":"eslint.detect-non-literal-regexp","url":"https://semgrep.dev/r/gitlab.eslint.detect-non-literal-regexp"},"identifiers":[{"project_id":5,"fingerprint":"08de3511f2132da4d24f1b8b1d3ca14368a0259b","external_type":"owasp","external_id":"A1:2017","name":"A1:2017 - Injection","url":null},{"project_id":5,"fingerprint":"7153fe286fd77c7a6250aa9603b82d44ab1c31e4","external_type":"cwe","external_id":"185","name":"CWE-185","url":"https://cwe.mitre.org/data/definitions/185.html"},{"project_id":5,"fingerprint":"a15f44ab746431d58b21b4fc67d8c4d3fb160ca0","external_type":"eslint_rule_id","external_id":"detect-non-literal-regexp","name":"ESLint rule ID/detect-non-literal-regexp","url":null},{"project_id":5,"fingerprint":"a751f35f1185de7ca5e6c0610c3bca21eb25ac9a","external_type":"semgrep_id","external_id":"eslint.detect-non-literal-regexp","name":"eslint.detect-non-literal-regexp","url":"https://semgrep.dev/r/gitlab.eslint.detect-non-literal-regexp"},{"project_id":5,"fingerprint":"a8e828eea3aba35916401da9304619f0a218119b","external_type":"owasp","external_id":"A03:2021","name":"A03:2021 - Injection","url":null}],"initial_finding_pipeline":{"iid":438},"latest_finding_pipeline":{"iid":438}},"vulnerability_read":{"project_id":5,"scanner":{"project_id":5,"external_id":"semgrep","name":"Semgrep","vendor":"GitLab"}}}
{"project_id":5,"author_id":1,"title":"Regular expression with non-literal value","description":null,"severity":"medium","report_type":"sast","vulnerability_finding":{"severity":"medium","report_type":"sast","project_id":5,"location_fingerprint":"4f7a2fffbb791c4cc8d1454db40b80f7fa9ed5be","name":"Regular expression with non-literal value","metadata_version":"15.1.4","raw_metadata":"{\"id\":\"b13b66b99eabefb8bc0d385b90cb952734e246ff3477a8ee563d6d04ef4bded4\",\"category\":\"sast\",\"name\":\"Regular expression with non-literal value\",\"description\":\"The `RegExp` constructor was called with a non-literal value. If an adversary were able to\\nsupply a malicious regex, they could cause a Regular Expression Denial of Service (ReDoS)\\nagainst the application. In Node applications, this could cause the entire application to no\\nlonger be responsive to other users' requests.\\n\\nTo remediate this issue, never allow user-supplied regular expressions. Instead, the regular \\nexpression should be hardcoded. If this is not possible, consider using an alternative regular\\nexpression engine such as [node-re2](https://www.npmjs.com/package/re2). RE2 is a safe alternative \\nthat does not support backtracking, which is what leads to ReDoS.\\n\\nExample using re2 which does not support backtracking (Note: it is still recommended to\\nnever use user-supplied input):\\n```\\n// Import the re2 module\\nconst RE2 = require('re2');\\n\\nfunction match(userSuppliedRegex, userInput) {\\n // Create a RE2 object with the user supplied regex, this is relatively safe\\n // due to RE2 not supporting backtracking which can be abused to cause long running\\n // queries\\n var re = new RE2(userSuppliedRegex);\\n // Execute the regular expression against some userInput\\n var result = re.exec(userInput);\\n // Work with the result\\n}\\n```\\n\\nFor more information on Regular Expression DoS see:\\n- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS\\n\",\"cve\":\"semgrep_id:eslint.detect-non-literal-regexp:515:515\",\"severity\":\"Medium\",\"scanner\":{\"id\":\"semgrep\",\"name\":\"Semgrep\"},\"location\":{\"file\":\"common/static/ace/ext-language_tools.js\",\"start_line\":515},\"identifiers\":[{\"type\":\"semgrep_id\",\"name\":\"eslint.detect-non-literal-regexp\",\"value\":\"eslint.detect-non-literal-regexp\",\"url\":\"https://semgrep.dev/r/gitlab.eslint.detect-non-literal-regexp\"},{\"type\":\"cwe\",\"name\":\"CWE-185\",\"value\":\"185\",\"url\":\"https://cwe.mitre.org/data/definitions/185.html\"},{\"type\":\"owasp\",\"name\":\"A03:2021 - Injection\",\"value\":\"A03:2021\"},{\"type\":\"owasp\",\"name\":\"A1:2017 - Injection\",\"value\":\"A1:2017\"},{\"type\":\"eslint_rule_id\",\"name\":\"ESLint rule ID/detect-non-literal-regexp\",\"value\":\"detect-non-literal-regexp\"}],\"tracking\":{\"type\":\"source\",\"items\":[{\"file\":\"common/static/ace/ext-language_tools.js\",\"line_start\":515,\"line_end\":515,\"signatures\":[{\"algorithm\":\"scope_offset\",\"value\":\"common/static/ace/ext-language_tools.js|func[0]:498\"}]}]}}","detection_method":"gitlab_security_report","uuid":"fa74cb01-2544-5d42-b9e8-0150119bf6cb","scanner":{"project_id":5,"external_id":"semgrep","name":"Semgrep","vendor":"GitLab"},"primary_identifier":{"project_id":5,"fingerprint":"a751f35f1185de7ca5e6c0610c3bca21eb25ac9a","external_type":"semgrep_id","external_id":"eslint.detect-non-literal-regexp","name":"eslint.detect-non-literal-regexp","url":"https://semgrep.dev/r/gitlab.eslint.detect-non-literal-regexp"},"identifiers":[{"project_id":5,"fingerprint":"08de3511f2132da4d24f1b8b1d3ca14368a0259b","external_type":"owasp","external_id":"A1:2017","name":"A1:2017 - Injection","url":null},{"project_id":5,"fingerprint":"7153fe286fd77c7a6250aa9603b82d44ab1c31e4","external_type":"cwe","external_id":"185","name":"CWE-185","url":"https://cwe.mitre.org/data/definitions/185.html"},{"project_id":5,"fingerprint":"a15f44ab746431d58b21b4fc67d8c4d3fb160ca0","external_type":"eslint_rule_id","external_id":"detect-non-literal-regexp","name":"ESLint rule ID/detect-non-literal-regexp","url":null},{"project_id":5,"fingerprint":"a751f35f1185de7ca5e6c0610c3bca21eb25ac9a","external_type":"semgrep_id","external_id":"eslint.detect-non-literal-regexp","name":"eslint.detect-non-literal-regexp","url":"https://semgrep.dev/r/gitlab.eslint.detect-non-literal-regexp"},{"project_id":5,"fingerprint":"a8e828eea3aba35916401da9304619f0a218119b","external_type":"owasp","external_id":"A03:2021","name":"A03:2021 - Injection","url":null}],"initial_finding_pipeline":{"iid":438},"latest_finding_pipeline":{"iid":438}},"vulnerability_read":{"project_id":5,"scanner":{"project_id":5,"external_id":"semgrep","name":"Semgrep","vendor":"GitLab"}}}
{"project_id":5,"author_id":1,"title":"Incorrect regular expression","description":null,"severity":"medium","report_type":"sast","vulnerability_finding":{"severity":"medium","report_type":"sast","project_id":5,"location_fingerprint":"f866afbfc47ac0fae0da7c6df8e5ed35330e4384","name":"Incorrect regular expression","metadata_version":"15.1.4","raw_metadata":"{\"id\":\"0152dfdd49aa1b9636cd267c12d080250199f15f21f427d3bed1a07a002e011f\",\"category\":\"sast\",\"name\":\"Incorrect regular expression\",\"description\":\"Ensure that the regex used to compare with user supplied input is safe from regular expression denial of service.\\n\",\"cve\":\"semgrep_id:nodejs_scan.javascript-dos-rule-regex_dos:1050:1052\",\"severity\":\"Medium\",\"scanner\":{\"id\":\"semgrep\",\"name\":\"Semgrep\"},\"location\":{\"file\":\"themis/static/assets/plugins/jquery-ui/ui/jquery.ui.sortable.js\",\"start_line\":1050,\"end_line\":1052},\"identifiers\":[{\"type\":\"semgrep_id\",\"name\":\"nodejs_scan.javascript-dos-rule-regex_dos\",\"value\":\"nodejs_scan.javascript-dos-rule-regex_dos\"},{\"type\":\"cwe\",\"name\":\"CWE-185\",\"value\":\"185\",\"url\":\"https://cwe.mitre.org/data/definitions/185.html\"},{\"type\":\"owasp\",\"name\":\"A05:2021 - Security Misconfiguration\",\"value\":\"A05:2021\"},{\"type\":\"owasp\",\"name\":\"A6:2017 - Security Misconfiguration\",\"value\":\"A6:2017\"},{\"type\":\"njsscan_rule_type\",\"name\":\"NodeJS Scan ID javascript-dos-rule-regex_dos\",\"value\":\"Ensure that the regex used to compare with user supplied input is safe from regular expression denial of service.\"}],\"tracking\":{\"type\":\"source\",\"items\":[{\"file\":\"themis/static/assets/plugins/jquery-ui/ui/jquery.ui.sortable.js\",\"line_start\":1050,\"line_end\":1050,\"signatures\":[{\"algorithm\":\"scope_offset\",\"value\":\"themis/static/assets/plugins/jquery-ui/ui/jquery.ui.sortable.js|func($, undefined)[0]:1034\"}]}]}}","detection_method":"gitlab_security_report","uuid":"fa2589df-c1ad-5108-93f0-90237b17c1b1","scanner":{"project_id":5,"external_id":"semgrep","name":"Semgrep","vendor":"GitLab"},"primary_identifier":{"project_id":5,"fingerprint":"ad9e1d2b073e1c296088e8fbedf8ed738d06f88a","external_type":"semgrep_id","external_id":"nodejs_scan.javascript-dos-rule-regex_dos","name":"nodejs_scan.javascript-dos-rule-regex_dos","url":null},"identifiers":[{"project_id":5,"fingerprint":"2bd02e525f0e78f8745e5a063ca1b5f396527a41","external_type":"owasp","external_id":"A6:2017","name":"A6:2017 - Security Misconfiguration","url":null},{"project_id":5,"fingerprint":"3f2c4e94cf8c0b53c44cb5b187963b753da9e882","external_type":"owasp","external_id":"A05:2021","name":"A05:2021 - Security Misconfiguration","url":null},{"project_id":5,"fingerprint":"518290ee3e47f4a5bba33213ca8a82e4c0d8697d","external_type":"njsscan_rule_type","external_id":"Ensure that the regex used to compare with user supplied input is safe from regular expression denial of service.","name":"NodeJS Scan ID javascript-dos-rule-regex_dos","url":null},{"project_id":5,"fingerprint":"7153fe286fd77c7a6250aa9603b82d44ab1c31e4","external_type":"cwe","external_id":"185","name":"CWE-185","url":"https://cwe.mitre.org/data/definitions/185.html"},{"project_id":5,"fingerprint":"ad9e1d2b073e1c296088e8fbedf8ed738d06f88a","external_type":"semgrep_id","external_id":"nodejs_scan.javascript-dos-rule-regex_dos","name":"nodejs_scan.javascript-dos-rule-regex_dos","url":null}],"initial_finding_pipeline":{"iid":438},"latest_finding_pipeline":{"iid":438}},"vulnerability_read":{"project_id":5,"scanner":{"project_id":5,"external_id":"semgrep","name":"Semgrep","vendor":"GitLab"}}}
{"project_id":5,"author_id":1,"title":"Regular expression with non-literal value","description":null,"severity":"medium","report_type":"sast","vulnerability_finding":{"severity":"medium","report_type":"sast","project_id":5,"location_fingerprint":"708aa3150b2b448e6894dd447689336d0ce63f19","name":"Regular expression with non-literal value","metadata_version":"15.1.4","raw_metadata":"{\"id\":\"f8c645cd515f94924c9a8fe73cc3e2bcf08b90ee9936462b6da57b6c28b52803\",\"category\":\"sast\",\"name\":\"Regular expression with non-literal value\",\"description\":\"The `RegExp` constructor was called with a non-literal value. If an adversary were able to\\nsupply a malicious regex, they could cause a Regular Expression Denial of Service (ReDoS)\\nagainst the application. In Node applications, this could cause the entire application to no\\nlonger be responsive to other users' requests.\\n\\nTo remediate this issue, never allow user-supplied regular expressions. Instead, the regular \\nexpression should be hardcoded. If this is not possible, consider using an alternative regular\\nexpression engine such as [node-re2](https://www.npmjs.com/package/re2). RE2 is a safe alternative \\nthat does not support backtracking, which is what leads to ReDoS.\\n\\nExample using re2 which does not support backtracking (Note: it is still recommended to\\nnever use user-supplied input):\\n```\\n// Import the re2 module\\nconst RE2 = require('re2');\\n\\nfunction match(userSuppliedRegex, userInput) {\\n // Create a RE2 object with the user supplied regex, this is relatively safe\\n // due to RE2 not supporting backtracking which can be abused to cause long running\\n // queries\\n var re = new RE2(userSuppliedRegex);\\n // Execute the regular expression against some userInput\\n var result = re.exec(userInput);\\n // Work with the result\\n}\\n```\\n\\nFor more information on Regular Expression DoS see:\\n- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS\\n\",\"cve\":\"semgrep_id:eslint.detect-non-literal-regexp:1108:1108\",\"severity\":\"Medium\",\"scanner\":{\"id\":\"semgrep\",\"name\":\"Semgrep\"},\"location\":{\"file\":\"themis/static/assets/plugins/jquery-ui/ui/jquery.ui.datepicker.js\",\"start_line\":1108},\"identifiers\":[{\"type\":\"semgrep_id\",\"name\":\"eslint.detect-non-literal-regexp\",\"value\":\"eslint.detect-non-literal-regexp\",\"url\":\"https://semgrep.dev/r/gitlab.eslint.detect-non-literal-regexp\"},{\"type\":\"cwe\",\"name\":\"CWE-185\",\"value\":\"185\",\"url\":\"https://cwe.mitre.org/data/definitions/185.html\"},{\"type\":\"owasp\",\"name\":\"A03:2021 - Injection\",\"value\":\"A03:2021\"},{\"type\":\"owasp\",\"name\":\"A1:2017 - Injection\",\"value\":\"A1:2017\"},{\"type\":\"eslint_rule_id\",\"name\":\"ESLint rule ID/detect-non-literal-regexp\",\"value\":\"detect-non-literal-regexp\"}],\"tracking\":{\"type\":\"source\",\"items\":[{\"file\":\"themis/static/assets/plugins/jquery-ui/ui/jquery.ui.datepicker.js\",\"line_start\":1108,\"line_end\":1108,\"signatures\":[{\"algorithm\":\"scope_offset\",\"value\":\"themis/static/assets/plugins/jquery-ui/ui/jquery.ui.datepicker.js|func($, undefined)[0]|getNumber[0]:4\"}]}]}}","detection_method":"gitlab_security_report","uuid":"f72b22e4-1e01-5c53-95a5-5e3a1e2f2b16","scanner":{"project_id":5,"external_id":"semgrep","name":"Semgrep","vendor":"GitLab"},"primary_identifier":{"project_id":5,"fingerprint":"a751f35f1185de7ca5e6c0610c3bca21eb25ac9a","external_type":"semgrep_id","external_id":"eslint.detect-non-literal-regexp","name":"eslint.detect-non-literal-regexp","url":"https://semgrep.dev/r/gitlab.eslint.detect-non-literal-regexp"},"identifiers":[{"project_id":5,"fingerprint":"08de3511f2132da4d24f1b8b1d3ca14368a0259b","external_type":"owasp","external_id":"A1:2017","name":"A1:2017 - Injection","url":null},{"project_id":5,"fingerprint":"7153fe286fd77c7a6250aa9603b82d44ab1c31e4","external_type":"cwe","external_id":"185","name":"CWE-185","url":"https://cwe.mitre.org/data/definitions/185.html"},{"project_id":5,"fingerprint":"a15f44ab746431d58b21b4fc67d8c4d3fb160ca0","external_type":"eslint_rule_id","external_id":"detect-non-literal-regexp","name":"ESLint rule ID/detect-non-literal-regexp","url":null},{"project_id":5,"fingerprint":"a751f35f1185de7ca5e6c0610c3bca21eb25ac9a","external_type":"semgrep_id","external_id":"eslint.detect-non-literal-regexp","name":"eslint.detect-non-literal-regexp","url":"https://semgrep.dev/r/gitlab.eslint.detect-non-literal-regexp"},{"project_id":5,"fingerprint":"a8e828eea3aba35916401da9304619f0a218119b","external_type":"owasp","external_id":"A03:2021","name":"A03:2021 - Injection","url":null}],"initial_finding_pipeline":{"iid":438},"latest_finding_pipeline":{"iid":438}},"vulnerability_read":{"project_id":5,"scanner":{"project_id":5,"external_id":"semgrep","name":"Semgrep","vendor":"GitLab"}}}

View File

@ -137,7 +137,6 @@ describe('~/frontend/environments/graphql/resolvers', () => {
);
expect(mockInitConnectionFn).toHaveBeenCalledWith({
configuration,
message: {
watchId: `kustomizations-${resourceName}`,
watchParams: {

View File

@ -154,7 +154,6 @@ describe('~/frontend/environments/graphql/resolvers', () => {
await mockResolvers.Query.k8sPods(null, { configuration, namespace }, { client });
expect(mockInitConnectionFn).toHaveBeenCalledWith({
configuration,
message: {
watchId: `k8sPods-n-${namespace}`,
watchParams: {
@ -281,7 +280,6 @@ describe('~/frontend/environments/graphql/resolvers', () => {
await mockResolvers.Query.k8sServices(null, { configuration, namespace }, { client });
expect(mockInitConnectionFn).toHaveBeenCalledWith({
configuration,
message: {
watchId: `k8sServices-n-${namespace}`,
watchParams: {
@ -546,7 +544,6 @@ describe('~/frontend/environments/graphql/resolvers', () => {
);
expect(mockInitConnectionFn).toHaveBeenCalledWith({
configuration,
message: {
watchId: `events-io-${involvedObjectName}`,
watchParams: {

View File

@ -0,0 +1,40 @@
import { WebSocketWatchManager } from '@gitlab/cluster-client';
import {
getWatchManager,
resetWatchManager,
} from '~/environments/services/websocket_connection_service';
jest.mock('@gitlab/cluster-client', () => ({
WebSocketWatchManager: jest.fn(),
}));
describe('getWatchManager', () => {
beforeEach(() => {
resetWatchManager();
});
it('creates new WebSocketWatchManager instance when configuration is provided', () => {
const mockConfig = { url: 'wss://example.com' };
const watchManager = getWatchManager(mockConfig);
expect(WebSocketWatchManager).toHaveBeenCalledWith(mockConfig);
expect(watchManager).toBeDefined();
});
it('returns existing instance when called multiple times', () => {
const mockConfig = { url: 'wss://example.com' };
const firstInstance = getWatchManager(mockConfig);
const secondInstance = getWatchManager();
expect(WebSocketWatchManager).toHaveBeenCalledTimes(1);
expect(firstInstance).toBe(secondInstance);
});
it('throws error when called without configuration and no existing instance', () => {
expect(() => {
getWatchManager();
}).toThrow('WebSocketWatchManager not initialized. Provide configuration first.');
});
});

View File

@ -1,4 +1,10 @@
import { handleClusterError } from '~/kubernetes_dashboard/graphql/helpers/resolver_helpers';
import {
handleClusterError,
subscribeToSocket,
} from '~/kubernetes_dashboard/graphql/helpers/resolver_helpers';
import { getWatchManager } from '~/environments/services/websocket_connection_service';
jest.mock('~/environments/services/websocket_connection_service');
describe('handleClusterError', () => {
describe('helper argument includes a response data', () => {
@ -46,3 +52,57 @@ describe('handleClusterError', () => {
});
});
});
describe('subscribeToSocket', () => {
const config = { apiUrl: 'test-url' };
const watchParams = { resource: 'pods', namespace: 'default' };
const watchId = 'test-watch-id';
let mockWatcher;
let mockCacheParams;
beforeEach(() => {
mockWatcher = {
on: jest.fn(),
initConnection: jest.fn(),
};
mockCacheParams = {
updateQueryCache: jest.fn(),
updateConnectionStatusFn: jest.fn(),
};
getWatchManager.mockReturnValue(mockWatcher);
mockWatcher.initConnection.mockResolvedValue(mockWatcher);
});
afterEach(() => {
jest.clearAllMocks();
});
it('initializes websocket connection with correct parameters', async () => {
await subscribeToSocket({
watchId,
watchParams,
cacheParams: mockCacheParams,
config,
});
expect(getWatchManager).toHaveBeenCalledWith(config);
expect(mockWatcher.initConnection).toHaveBeenCalledWith({
message: { watchId, watchParams },
});
});
it('handles connection initialization failure', async () => {
mockWatcher.initConnection.mockRejectedValue(new Error('Connection failed'));
await expect(
subscribeToSocket({
watchId,
watchParams,
cacheParams: mockCacheParams,
config,
}),
).rejects.toThrow('Failed to establish WebSocket connection');
});
});

View File

@ -1391,14 +1391,16 @@ RSpec.describe API::Helpers, feature_category: :shared do
let(:content_type) { nil }
let(:content_disposition) { nil }
let(:extra_response_headers) { {} }
let(:extra_send_url_params) { {} }
subject do
helper.present_carrierwave_file!(
artifact.file,
supports_direct_download: supports_direct_download,
content_disposition: content_disposition,
content_type: content_type,
extra_response_headers: extra_response_headers
supports_direct_download:,
content_disposition:,
content_type:,
extra_response_headers:,
extra_send_url_params:
)
end
@ -1514,6 +1516,27 @@ RSpec.describe API::Helpers, feature_category: :shared do
subject
end
end
context 'with extra_send_url_params set' do
let(:extra_send_url_params) { { restrict_forwarded_response_headers: { enabled: true, allow_list: ['x-optional-header'] } } }
it 'sends a workhorse header with the response headers' do
expect(helper).to receive(:status).with(:ok)
expect(helper).to receive(:body).with('')
expect(helper).to receive(:header) do |name, value|
expect(name).to eq(Gitlab::Workhorse::SEND_DATA_HEADER)
command, encoded_params = value.split(":")
params = Gitlab::Json.parse(Base64.urlsafe_decode64(encoded_params))
expect(command).to eq('send-url')
restrict_forwarded_response_headers_params = params['RestrictForwardedResponseHeaders']
expect(restrict_forwarded_response_headers_params['Enabled']).to be_truthy
expect(restrict_forwarded_response_headers_params['AllowList']).to contain_exactly('x-optional-header')
end
subject
end
end
end
end
end

View File

@ -11,6 +11,12 @@ RSpec.describe Banzai::Filter::QuickActionFilter, feature_category: :markdown do
expect(result[:quick_action_paragraphs]).to match_array [{ start_line: 0, end_line: 1 }]
end
it 'detects action in paragraph when it is on another line' do
described_class.call(%(<p data-sourcepos="1:1-2:3">foo\n/quick</p>), {}, result)
expect(result[:quick_action_paragraphs]).to match_array [{ start_line: 0, end_line: 1 }]
end
it 'does not detect action in paragraph if no sourcepos' do
described_class.call('<p>/quick</p>', {}, result)

View File

@ -293,6 +293,22 @@ RSpec.describe Gitlab::ImportExport::MembersMapper do
end
end
context 'when not using the default member' do
let(:members_mapper) do
described_class.new(
exported_members: exported_members, user: user2, importable: importable, default_member: false
)
end
before do
importable.add_members([user, user2], GroupMember::MAINTAINER)
end
it 'does not destroy existing members on initialization' do
expect { members_mapper }.not_to change { importable.reload.members.count }
end
end
context 'when importer mapping fails' do
let(:exception_message) { 'Something went wrong' }

View File

@ -1110,7 +1110,6 @@ Vulnerabilities::Finding:
- severity
- report_type
- project_id
- project_fingerprint
- location_fingerprint
- name
- metadata_version

View File

@ -0,0 +1,20 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Middleware::SecureHeaders, feature_category: :shared do
let(:status_origin) { 200 }
let(:body_origin) { ['Hello, World'] }
let(:app) { ->(_env) { [status_origin, {}, body_origin] } }
let(:env) { Rack::MockRequest.env_for('/', method: 'get') }
subject(:middleware) { described_class.new(app) }
it 'adds the expected header to the response and preserves the original response status and body' do
status, headers, body = middleware.call(env)
expect(headers['NEL']).to eq('{"max_age": 0}')
expect(status).to eq(status_origin)
expect(body).to eq(body_origin)
end
end

View File

@ -548,7 +548,11 @@ RSpec.describe Gitlab::Workhorse, feature_category: :shared do
'Header' => {},
'ResponseHeaders' => {},
'Body' => '',
'Method' => 'GET'
'Method' => 'GET',
'RestrictForwardedResponseHeaders' => {
'Enabled' => false,
'AllowList' => []
}
}
end
@ -649,6 +653,26 @@ RSpec.describe Gitlab::Workhorse, feature_category: :shared do
expect(params).to eq(expected_params)
end
end
context 'when `restrict_forwarded_repsonse_headers` is set' do
let(:expected_params) do
super().merge('RestrictForwardedResponseHeaders' => {
'Enabled' => true,
'AllowList' => ['x-optional-header']
})
end
it 'sets the header correctly' do
key, command, params = decode_workhorse_header(described_class.send_url(
url,
restrict_forwarded_response_headers: { enabled: true, allow_list: ['x-optional-header'] }
))
expect(key).to eq('Gitlab-Workhorse-Send-Data')
expect(command).to eq('send-url')
expect(params).to eq(expected_params)
end
end
end
describe '.send_scaled_image' do
@ -682,10 +706,12 @@ RSpec.describe Gitlab::Workhorse, feature_category: :shared do
let(:ssrf_filter) { false }
let(:allow_localhost) { true }
let(:allowed_uris) { [] }
let(:restrict_forwarded_response_headers) { {} }
let(:expected_restrict_forwarded_response_headers) { { 'Enabled' => false, 'AllowList' => [] } }
subject do
described_class.send_dependency(
headers, url, upload_config: upload_config, ssrf_filter: ssrf_filter, allow_localhost: allow_localhost, allowed_uris: allowed_uris
headers, url, upload_config:, ssrf_filter:, allow_localhost:, allowed_uris:, restrict_forwarded_response_headers:
)
end
@ -698,6 +724,7 @@ RSpec.describe Gitlab::Workhorse, feature_category: :shared do
'SSRFFilter' => ssrf_filter,
'AllowedURIs' => allowed_uris.map(&:to_s),
'Url' => url,
'RestrictForwardedResponseHeaders' => expected_restrict_forwarded_response_headers,
'UploadConfig' => {
'Method' => upload_method,
'Url' => upload_url,
@ -758,6 +785,18 @@ RSpec.describe Gitlab::Workhorse, feature_category: :shared do
it_behaves_like 'setting the header correctly'
end
context 'when `restrict_forwarded_response_headers` parameter is set' do
let(:restrict_forwarded_response_headers) { { enabled: true, allow_list: ['x-optional-header'] } }
let(:expected_restrict_forwarded_response_headers) do
super().merge(
'Enabled' => true,
'AllowList' => ['x-optional-header']
)
end
it_behaves_like 'setting the header correctly'
end
end
describe '.send_git_snapshot' do

View File

@ -177,6 +177,20 @@ RSpec.describe Groups::UpdateService, feature_category: :groups_and_projects do
expect(updated_group.crm_enabled?).to be_truthy
expect(updated_group.crm_group).to eq(internal_group)
end
context 'when crm_source_group_id blank and issues have contacts' do
let(:params) { { crm_source_group_id: '' } }
before do
allow(public_group).to receive(:has_issues_with_contacts?).and_return(true)
end
it 'does not return an error' do
described_class.new(public_group, user, params).execute
expect(public_group.errors).to be_empty
end
end
end
context 'with existing crm_settings' do

View File

@ -13,6 +13,23 @@ RSpec.shared_examples 'returns failure response' do |expected_status, expected_m
end
end
RSpec.shared_examples 'returns success response' do
it 'schedules a restore of the relation' do
expect(Projects::ImportExport::RelationImportWorker).to receive(:perform_async)
import_service.execute
end
it 'returns a service response' do
response = import_service.execute
expect(response).to be_instance_of(ServiceResponse)
expect(response).to be_success
expect(response.http_status).to eq(:ok)
expect(response.payload).to be_instance_of(Projects::ImportExport::RelationImportTracker)
end
end
RSpec.describe ::Projects::ImportExport::RelationImportService, :aggregate_failures, feature_category: :importers do
let_it_be(:project) { create(:project) }
@ -39,20 +56,7 @@ RSpec.describe ::Projects::ImportExport::RelationImportService, :aggregate_failu
project.add_maintainer(user)
end
it 'schedules a restore of the relation' do
expect(Projects::ImportExport::RelationImportWorker).to receive(:perform_async)
import_service.execute
end
it 'returns a service response' do
response = import_service.execute
expect(response).to be_instance_of(ServiceResponse)
expect(response).to be_success
expect(response.http_status).to eq(:ok)
expect(response.payload).to be_instance_of(Projects::ImportExport::RelationImportTracker)
end
include_examples 'returns success response'
context 'and the relation import tracker cannot be created' do
before do
@ -66,6 +70,12 @@ RSpec.describe ::Projects::ImportExport::RelationImportService, :aggregate_failu
end
end
context 'and the user is an admin bot' do
let_it_be(:user) { Users::Internal.admin_bot }
include_examples 'returns success response'
end
context 'and the user has developer access' do
before_all do
project.add_developer(user)
@ -74,7 +84,7 @@ RSpec.describe ::Projects::ImportExport::RelationImportService, :aggregate_failu
include_examples 'returns failure response', :forbidden, 'You are not authorized to perform this action'
end
context 'and the has no access' do
context 'and the user has no access' do
include_examples 'returns failure response', :forbidden, 'You are not authorized to perform this action'
end

View File

@ -6,7 +6,6 @@ module MigrationHelpers
uuid = SecureRandom.uuid
{
project_fingerprint: SecureRandom.hex(20),
location_fingerprint: Digest::SHA1.hexdigest(SecureRandom.hex(10)), # rubocop:disable Fips/SHA1
uuid: uuid,
name: "Vulnerability Finding #{uuid}",

View File

@ -6,7 +6,7 @@ module MigrationHelpers
def create_finding!(
project_id:, scanner_id:, primary_identifier_id:, vulnerability_id: nil,
name: "test", severity: 7, report_type: 0,
project_fingerprint: '123qweasdzxc', location_fingerprint: 'test',
location_fingerprint: 'test',
metadata_version: 'test', raw_metadata: 'test', uuid: 'b1cee17e-3d7a-11ed-b878-0242ac120002')
table(:vulnerability_occurrences).create!(
vulnerability_id: vulnerability_id,
@ -14,7 +14,6 @@ module MigrationHelpers
name: name,
severity: severity,
report_type: report_type,
project_fingerprint: project_fingerprint,
scanner_id: scanner_id,
primary_identifier_id: primary_identifier_id,
location_fingerprint: location_fingerprint,

View File

@ -179,6 +179,13 @@ RSpec.shared_examples 'sanitize link' do
expect(act.to_html).to eq exp
end
it 'limits to MAX_LINK_ATTRIBUTES links' do
exp = %q(<a href="foo/bar.md">foo/bar.md</a>) * (Banzai::Filter::SanitizeLinkFilter::MAX_LINK_ATTRIBUTES + 1)
act = filter(exp)
expect(act.to_html).to eq Banzai::Filter::SanitizeLinkFilter::TIMEOUT_MARKDOWN_MESSAGE
end
end
# not meant to be exhaustive, but verify that the pipeline is doing sanitization

View File

@ -34,10 +34,18 @@ RSpec.describe Projects::ImportExport::RelationImportWorker, feature_category: :
end
it 'refreshes the project stats' do
expect(worker).to receive(:perform_post_import_tasks)
allow(worker).to receive(:project).and_return(tracker.project)
expect(tracker.project).to receive(:reset_counters_and_iids)
expect(InternalId).to receive(:flush_records!).with(namespace: tracker.project.project_namespace)
perform
end
it 'does not change any project attributes' do
tracker.project.update!(description: 'an updated description', approvals_before_merge: 2, visibility_level: 10)
expect { perform }.not_to change { tracker.project.reload.attributes }
end
end
context 'when the import fails' do

View File

@ -7,8 +7,7 @@ internal/api/channel_settings.go:57:28: G402: TLS MinVersion too low. (gosec)
internal/channel/channel.go:128:31: response body must be closed (bodyclose)
internal/config/config.go:247:18: G204: Subprocess launched with variable (gosec)
internal/config/config.go:339:8: G101: Potential hardcoded credentials (gosec)
internal/dependencyproxy/dependencyproxy.go:121: Function 'Inject' is too long (61 > 60) (funlen)
internal/dependencyproxy/dependencyproxy_test.go:514: internal/dependencyproxy/dependencyproxy_test.go:514: Line contains TODO/BUG/FIXME/NOTE/OPTIMIZE/HACK: "note that the timeout duration here is s..." (godox)
internal/dependencyproxy/dependencyproxy_test.go:572: internal/dependencyproxy/dependencyproxy_test.go:572: Line contains TODO/BUG/FIXME/NOTE/OPTIMIZE/HACK: "note that the timeout duration here is s..." (godox)
internal/git/archive.go:67: Function 'Inject' has too many statements (55 > 40) (funlen)
internal/git/blob.go:21:5: exported: exported var SendBlob should have comment or be unexported (revive)
internal/git/diff.go:1: 1-47 lines are duplicate of `internal/git/format-patch.go:1-48` (dupl)

View File

@ -15,6 +15,7 @@ import (
"gitlab.com/gitlab-org/labkit/log"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/api"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/forwardheaders"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/helper/fail"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/senddata"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/transport"
@ -49,13 +50,14 @@ type Injector struct {
}
type entryParams struct {
URL string
Headers http.Header
ResponseHeaders http.Header
UploadConfig uploadConfig
SSRFFilter bool
AllowLocalhost bool
AllowedURIs []string
URL string
Headers http.Header
ResponseHeaders http.Header
UploadConfig uploadConfig
SSRFFilter bool
AllowLocalhost bool
AllowedURIs []string
RestrictForwardedResponseHeaders forwardheaders.Params
}
type uploadConfig struct {
@ -160,8 +162,7 @@ func (p *Injector) Inject(w http.ResponseWriter, r *http.Request, sendData strin
}
forwardHeaders(dependencyResponse.Header, saveFileRequest)
p.forwardHeadersToResponse(w, dependencyResponse.Header, params.ResponseHeaders)
params.RestrictForwardedResponseHeaders.ForwardResponseHeaders(w, dependencyResponse, []string{}, params.ResponseHeaders)
// workhorse hijack overwrites the Content-Type header, but we need this header value
saveFileRequest.Header.Set("Workhorse-Proxy-Content-Type", dependencyResponse.Header.Get("Content-Type"))
@ -234,17 +235,6 @@ func (p *Injector) newUploadRequest(ctx context.Context, params *entryParams, or
return request, nil
}
func (p *Injector) forwardHeadersToResponse(w http.ResponseWriter, headers ...http.Header) {
for _, h := range headers {
for key, values := range h {
w.Header().Del(key)
for _, v := range values {
w.Header().Add(key, v)
}
}
}
}
func (p *Injector) unpackParams(sendData string) (*entryParams, error) {
var params entryParams
if err := p.Unpack(&params, sendData); err != nil {

View File

@ -437,6 +437,64 @@ func mergeMap(from map[string]interface{}, into map[string]interface{}) map[stri
return into
}
func TestRestrictForwardedResponseHeaders(t *testing.T) {
content := []byte("result")
contentLength := strconv.Itoa(len(content))
contentType := "multipart/x-mixed-replace"
acceptedHeader := "X-Accepted-Header"
notAcceptedHeader := "X-Not-Accepted-Header"
originResourceServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Length", contentLength)
w.Header().Set("Content-Type", contentType)
w.Header().Set(acceptedHeader, "test")
w.Header().Set(notAcceptedHeader, "test")
w.Write(content)
}))
defer originResourceServer.Close()
uploadHandler := &fakeUploadHandler{
handler: func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(200)
},
}
injector := NewInjector()
injector.SetUploadHandler(uploadHandler)
entryParamsJSON := jsonEntryParams(t, &map[string]interface{}{
"Token": "token",
"Url": originResourceServer.URL + `/remote/file`,
"ResponseHeaders": http.Header{"CustomHeader": {"test"}},
"RestrictForwardedResponseHeaders": &map[string]interface{}{
"Enabled": true,
"AllowList": []string{acceptedHeader, "Content-Type", "Content-Length"},
},
})
response := makeRequest(injector, entryParamsJSON)
require.Equal(t, "/target/upload", uploadHandler.request.URL.Path)
require.Equal(t, int64(6), uploadHandler.request.ContentLength)
require.Equal(t, content, uploadHandler.body)
require.Equal(t, http.StatusOK, response.Code)
expectedHeaders := http.Header{
"Content-Length": []string{contentLength},
"Content-Type": []string{"application/octet-stream"},
acceptedHeader: []string{"test"},
"Customheader": []string{"test"},
}
require.Equal(t, expectedHeaders, response.Header())
}
func jsonEntryParams(t *testing.T, params *map[string]interface{}) string {
result, err := json.Marshal(params)
require.NoError(t, err)
return string(result)
}
func TestIncorrectSendData(t *testing.T) {
response := makeRequest(NewInjector(), "")

View File

@ -0,0 +1,75 @@
// Package forwardheaders implements utility functions for forwarding headers from
// a response to a response writer
//
// This is meant to be used in sendurl and dependencyproxy packages.
package forwardheaders
import (
"net/http"
"slices"
"strings"
)
const (
contentTypeHeader = "Content-Type"
octetStreamMimeType = "application/octet-stream"
)
// Params represents the configuration used by this package.
type Params struct {
Enabled bool
AllowList []string
}
// ForwardResponseHeaders will forward the headers from the passed upstream response to the response writer.
// If a header is forwarded or not depends on a few rules:
// * if a header is present in the preserveHeaderKeys slice, then it's not forwarded.
// * if enabled _and_ a header is present in the allow list, then it's forward.
// * if disabled, all headers (except those that are protected) are forwarded.
func (p *Params) ForwardResponseHeaders(w http.ResponseWriter, upstreamResponse *http.Response, preserveHeaderKeys []string, extraHeaders http.Header) {
if p.Enabled {
replaceContentType(upstreamResponse)
}
w.Header().Del("Content-Length")
canonicalProtectedKeys := []string{}
canonicalAllowedKeys := []string{}
for _, header := range preserveHeaderKeys {
canonicalProtectedKeys = append(canonicalProtectedKeys, http.CanonicalHeaderKey(header))
}
for _, header := range p.AllowList {
canonicalAllowedKeys = append(canonicalAllowedKeys, http.CanonicalHeaderKey(header))
}
// forward headers according to the protected and allowed keys
for key, value := range upstreamResponse.Header {
if !slices.Contains(canonicalProtectedKeys, key) {
if p.Enabled {
if slices.Contains(canonicalAllowedKeys, key) {
w.Header()[key] = value
}
} else {
w.Header()[key] = value
}
}
}
// set the extra headers
for key, values := range extraHeaders {
w.Header().Del(key)
for _, value := range values {
w.Header().Add(key, value)
}
}
}
func replaceContentType(response *http.Response) {
contentType := response.Header.Get(contentTypeHeader)
if strings.HasPrefix(contentType, "multipart") {
response.Header.Set(contentTypeHeader, octetStreamMimeType)
}
}

View File

@ -0,0 +1,97 @@
package forwardheaders
import (
"net/http"
"net/http/httptest"
"testing"
"maps"
"github.com/stretchr/testify/require"
)
var upstreamHeaders = http.Header{
"X-Protected-Header1": []string{"protected1_from_upstream"},
"X-Protected-Header2": []string{"protected2_from_upstream"},
"X-Custom-Header1": []string{"custom1_from_upstream"},
"X-Custom-Header2": []string{"custom2_from_upstream"},
}
func TestForwardResponseHeaders(t *testing.T) {
testCases := []struct {
desc string
params Params
protectedHeaders []string
extraHeaders http.Header
upstreamContentType string
responseWriterHeaders http.Header
expectedResponseWriterHeaders http.Header
}{
{
desc: "with restricted headers enabled",
params: Params{Enabled: true, AllowList: []string{"content-type", "x-custom-header1"}},
protectedHeaders: []string{"x-protected-header1"},
extraHeaders: http.Header{"X-Extra-Header": []string{"test"}},
upstreamContentType: "multipart/x-mixed-replace",
responseWriterHeaders: http.Header{"X-Protected-Header1": []string{"protected1_from_response_writer"}},
expectedResponseWriterHeaders: http.Header{
"Content-Type": []string{"application/octet-stream"},
"X-Protected-Header1": []string{"protected1_from_response_writer"},
"X-Custom-Header1": []string{"custom1_from_upstream"},
"X-Extra-Header": []string{"test"},
},
},
{
desc: "with restrict headers disabled",
params: Params{Enabled: false, AllowList: []string{}},
protectedHeaders: []string{"x-protected-header1"},
extraHeaders: http.Header{"X-Extra-Header": []string{"test"}},
upstreamContentType: "multipart/x-mixed-replace",
responseWriterHeaders: http.Header{"X-Protected-Header1": []string{"protected1_from_response_writer"}},
expectedResponseWriterHeaders: http.Header{
"Content-Type": []string{"multipart/x-mixed-replace"},
"X-Protected-Header1": []string{"protected1_from_response_writer"},
"X-Protected-Header2": []string{"protected2_from_upstream"},
"X-Custom-Header1": []string{"custom1_from_upstream"},
"X-Custom-Header2": []string{"custom2_from_upstream"},
"X-Extra-Header": []string{"test"},
},
},
{
desc: "with no protected headers",
params: Params{Enabled: true, AllowList: []string{"content-type"}},
protectedHeaders: []string{},
extraHeaders: http.Header{"X-Extra-Header": []string{"test"}},
upstreamContentType: "multipart/x-mixed-replace",
responseWriterHeaders: http.Header{"X-Protected-Header1": []string{"protected1_from_response_writer"}},
expectedResponseWriterHeaders: http.Header{
"Content-Type": []string{"application/octet-stream"},
"X-Protected-Header1": []string{"protected1_from_response_writer"},
"X-Extra-Header": []string{"test"},
},
},
{
desc: "with non restricted content-type",
params: Params{Enabled: true, AllowList: []string{"content-type"}},
protectedHeaders: []string{},
extraHeaders: http.Header{},
upstreamContentType: "application/xml",
responseWriterHeaders: http.Header{},
expectedResponseWriterHeaders: http.Header{
"Content-Type": []string{"application/xml"},
},
},
}
for _, tc := range testCases {
w := httptest.NewRecorder()
maps.Copy(w.Header(), tc.responseWriterHeaders)
upstreamResp := &http.Response{Header: upstreamHeaders}
upstreamResp.Header.Set("Content-Type", tc.upstreamContentType)
tc.params.ForwardResponseHeaders(w, upstreamResp, tc.protectedHeaders, tc.extraHeaders)
require.Equal(t, tc.expectedResponseWriterHeaders, w.Header())
}
}

View File

@ -16,6 +16,7 @@ import (
"gitlab.com/gitlab-org/labkit/mask"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/config"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/forwardheaders"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/helper/fail"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/log"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/senddata"
@ -25,19 +26,20 @@ import (
type entry struct{ senddata.Prefix }
type entryParams struct {
URL string
AllowRedirects bool
AllowLocalhost bool
AllowedURIs []string
SSRFFilter bool
DialTimeout config.TomlDuration
ResponseHeaderTimeout config.TomlDuration
ErrorResponseStatus int
TimeoutResponseStatus int
Body string
Header http.Header
ResponseHeaders http.Header
Method string
URL string
AllowRedirects bool
AllowLocalhost bool
AllowedURIs []string
SSRFFilter bool
DialTimeout config.TomlDuration
ResponseHeaderTimeout config.TomlDuration
ErrorResponseStatus int
TimeoutResponseStatus int
Body string
Header http.Header
ResponseHeaders http.Header
Method string
RestrictForwardedResponseHeaders forwardheaders.Params
}
type cacheKey struct {
@ -66,11 +68,11 @@ var rangeHeaderKeys = []string{
// Keep cache headers from the original response, not the proxied response. The
// original response comes from the Rails application, which should be the
// source of truth for caching.
var preserveHeaderKeys = map[string]bool{
"Cache-Control": true,
"Expires": true,
"Date": true, // Support for HTTP 1.0 proxies
"Pragma": true, // Support for HTTP 1.0 proxies
var preserveHeaderKeys = []string{
"Cache-Control",
"Expires",
"Date", // Support for HTTP 1.0 proxies
"Pragma", // Support for HTTP 1.0 proxies
}
var httpClientNoRedirect = func(_ *http.Request, _ []*http.Request) error {
@ -137,7 +139,7 @@ func (e *entry) Inject(w http.ResponseWriter, r *http.Request, sendData string)
e.handleRequestError(w, r, err, &params)
return
}
e.copyResponseHeaders(w, resp, params.ResponseHeaders)
params.RestrictForwardedResponseHeaders.ForwardResponseHeaders(w, resp, preserveHeaderKeys, params.ResponseHeaders)
w.WriteHeader(resp.StatusCode)
defer func() {
@ -196,23 +198,6 @@ func (e *entry) handleRequestError(w http.ResponseWriter, r *http.Request, err e
fail.Request(w, r, fmt.Errorf("SendURL: Do request: %v", err), fail.WithStatus(status))
}
func (e *entry) copyResponseHeaders(w http.ResponseWriter, resp *http.Response, responseHeaders map[string][]string) {
w.Header().Del("Content-Length")
for key, value := range resp.Header {
if !preserveHeaderKeys[key] {
w.Header()[key] = value
}
}
for key, values := range responseHeaders {
w.Header().Del(key)
for _, value := range values {
w.Header().Add(key, value)
}
}
}
func (e *entry) streamResponse(w http.ResponseWriter, body io.Reader) error {
n, err := io.Copy(newFlushingResponseWriter(w), body)
sendURLBytes.Add(float64(n))

View File

@ -18,6 +18,10 @@ import (
const testData = `123456789012345678901234567890`
const testDataEtag = `W/"myetag"`
const entryServerExtraHeader1 = "X-Custom-Header1"
const entryServerExtraHeader1Value = "Header1"
const entryServerExtraHeader2 = "X-Custom-Header2"
const entryServerExtraHeader2Value = "Header2"
type option struct {
Key string
@ -65,6 +69,8 @@ func testEntryServer(t *testing.T, requestURL string, httpHeaders http.Header, a
w.Header().Set("Expires", "Wed, 21 Oct 2015 07:28:00 GMT")
w.Header().Set("Date", "Wed, 21 Oct 2015 06:28:00 GMT")
w.Header().Set("Pragma", "")
w.Header().Set(entryServerExtraHeader1, entryServerExtraHeader1Value)
w.Header().Set(entryServerExtraHeader2, entryServerExtraHeader2Value)
http.ServeContent(w, r, "archive.txt", time.Now(), tempFile)
}
@ -330,3 +336,26 @@ func TestSSRFFilterWithAllowLocalhost(t *testing.T) {
require.Equal(t, http.StatusOK, response.Code)
}
func TestRestrictForwardedResponseHeaders(t *testing.T) {
restrictForwardedResponseHeadersParams := &map[string]interface{}{
"Enabled": true,
"AllowList": []string{entryServerExtraHeader1},
}
response := testEntryServer(t, "/get/request", nil, false, option{Key: "RestrictForwardedResponseHeaders", Value: restrictForwardedResponseHeadersParams}, option{Key: "ResponseHeaders", Value: http.Header{"CustomHeader": {"Test"}}})
require.Equal(t, http.StatusOK, response.Code)
expectedHeaders := http.Header{
"Content-Disposition": []string{"attachment; filename=\"archive.txt\""},
"Cache-Control": []string{"no-cache"},
"Expires": []string{""},
"Date": []string{"Wed, 21 Oct 2015 05:28:00 GMT"},
"Pragma": []string{"no-cache"},
entryServerExtraHeader1: []string{entryServerExtraHeader1Value},
"Customheader": []string{"Test"},
}
require.Equal(t, expectedHeaders, response.Header())
}

View File

@ -1365,12 +1365,11 @@
resolved "https://registry.yarnpkg.com/@gitlab/at.js/-/at.js-1.5.7.tgz#1ee6f838cc4410a1d797770934df91d90df8179e"
integrity sha512-c6ySRK/Ma7lxwpIVbSAF3P+xiTLrNTGTLRx4/pHK111AdFxwgUwrYF6aVZFXvmG65jHOJHoa0eQQ21RW6rm0Rg==
"@gitlab/cluster-client@^2.5.0":
version "2.5.0"
resolved "https://registry.yarnpkg.com/@gitlab/cluster-client/-/cluster-client-2.5.0.tgz#de5e3be45bbcd1da36e8337ace9d8e81521efa0b"
integrity sha512-uaVceqNLmAayvWLCjWUcYUkZ2AfWaj0yTWiu075sGt81zFKmWF+asz+vwWUXBClLKNaHZEoGrDkeJgOTlJP2HQ==
"@gitlab/cluster-client@^3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@gitlab/cluster-client/-/cluster-client-3.0.0.tgz#96351f55eef83c0dc3f5c030e2fc71d6c4753bd1"
integrity sha512-Hg9m9Kf38UUj/30PXxQY96SLVLqXd2mQeVDW1OYzHtOVOU59FXT8s9BydC2OR9J02+WHQQLMHYQKaMYO/wpIKg==
dependencies:
axios "^0.24.0"
core-js "^3.29.1"
mitt "^3.0.1"
@ -15035,10 +15034,10 @@ vite-plugin-ruby@^5.1.1:
debug "^4.3.4"
fast-glob "^3.3.2"
vite@^6.3.1:
version "6.3.1"
resolved "https://registry.yarnpkg.com/vite/-/vite-6.3.1.tgz#6b080ff907308ca691c5639c095ab6d938734c0d"
integrity sha512-kkzzkqtMESYklo96HKKPE5KKLkC1amlsqt+RjFMlX2AvbRB/0wghap19NdBxxwGZ+h/C6DLCrcEphPIItlGrRQ==
vite@^6.3.2:
version "6.3.2"
resolved "https://registry.yarnpkg.com/vite/-/vite-6.3.2.tgz#4c1bb01b1cea853686a191657bbc14272a038f0a"
integrity sha512-ZSvGOXKGceizRQIZSz7TGJ0pS3QLlVY/9hwxVh17W3re67je1RKYzFHivZ/t0tubU78Vkyb9WnHPENSBCzbckg==
dependencies:
esbuild "^0.25.0"
fdir "^6.4.3"