diff --git a/rhodecode/__init__.py b/rhodecode/__init__.py index 045bfff5..59954965 100644 --- a/rhodecode/__init__.py +++ b/rhodecode/__init__.py @@ -100,7 +100,7 @@ PYRAMID_SETTINGS = {} EXTENSIONS = {} __version__ = ".".join((str(each) for each in VERSION[:3])) -__dbversion__ = 121 # defines current db version for migrations +__dbversion__ = 123 # defines current db version for migrations __license__ = "AGPLv3, and Commercial License" __author__ = "RhodeCode GmbH" __url__ = "https://code.rhodecode.com" diff --git a/rhodecode/api/tests/test_get_users.py b/rhodecode/api/tests/test_get_users.py index b86aebcd..98c26bca 100644 --- a/rhodecode/api/tests/test_get_users.py +++ b/rhodecode/api/tests/test_get_users.py @@ -33,7 +33,7 @@ class TestGetUsers(object): ret_all = [] _users = ( User.query() - .filter(User.username.notin_([User.DEFAULT_USER, User.GHOST_USER, User.AI_USER])) + .filter(User.username.notin_([User.DEFAULT_USER, User.GHOST_USER, User.AI_USER, User.SECURITY_USER])) .order_by(User.username) .all() ) diff --git a/rhodecode/api/views/user_api.py b/rhodecode/api/views/user_api.py index 73d4c8fa..9905d56f 100644 --- a/rhodecode/api/views/user_api.py +++ b/rhodecode/api/views/user_api.py @@ -148,7 +148,7 @@ def get_users(request, apiuser): users_list = ( User.query() .order_by(User.username) - .filter(User.username.notin_([User.DEFAULT_USER, User.GHOST_USER, User.AI_USER])) + .filter(User.username.notin_([User.DEFAULT_USER, User.GHOST_USER, User.AI_USER, User.SECURITY_USER])) .all() ) for user in users_list: diff --git a/rhodecode/apps/_base/__init__.py b/rhodecode/apps/_base/__init__.py index f6ec8bbe..f7f7e18f 100644 --- a/rhodecode/apps/_base/__init__.py +++ b/rhodecode/apps/_base/__init__.py @@ -168,7 +168,7 @@ class BaseAppView(object): if not user_obj: return - if user_obj.username in [User.DEFAULT_USER, User.GHOST_USER, User.AI_USER]: + if user_obj.username in [User.DEFAULT_USER, User.GHOST_USER, User.AI_USER, User.SECURITY_USER]: return now = time.time() @@ -294,7 +294,7 @@ class RepoAppView(BaseAppView): def _prepare_and_set_clone_url(self, c): username = "" - if self._rhodecode_user.username not in [User.DEFAULT_USER, User.GHOST_USER, User.AI_USER]: + if self._rhodecode_user.username not in [User.DEFAULT_USER, User.GHOST_USER, User.AI_USER, User.SECURITY_USER]: username = self._rhodecode_user.username _def_clone_uri = c.clone_uri_tmpl @@ -591,7 +591,7 @@ class UserAppView(BaseAppView): _ = self.request.translate if not request.db_user_supports_default: - if self.db_user.username in [User.DEFAULT_USER, User.GHOST_USER, User.AI_USER]: + if self.db_user.username in [User.DEFAULT_USER, User.GHOST_USER, User.AI_USER, User.SECURITY_USER]: h.flash( _("Editing user `{}` is disabled.".format(self.db_user.username)), category="warning", diff --git a/rhodecode/apps/admin/tests/test_admin_users.py b/rhodecode/apps/admin/tests/test_admin_users.py index bc8c9fe2..d8a7095e 100644 --- a/rhodecode/apps/admin/tests/test_admin_users.py +++ b/rhodecode/apps/admin/tests/test_admin_users.py @@ -42,7 +42,9 @@ class TestAdminUsersView(TestController): response = self.app.get(route_path("users_data"), extra_environ=xhr_header) all_users = ( - User.query().filter(User.username.notin_([User.DEFAULT_USER, User.GHOST_USER, User.AI_USER])).count() + User.query() + .filter(User.username.notin_([User.DEFAULT_USER, User.GHOST_USER, User.AI_USER, User.SECURITY_USER])) + .count() ) assert response.json["recordsTotal"] == all_users @@ -53,7 +55,9 @@ class TestAdminUsersView(TestController): ) all_users = ( - User.query().filter(User.username.notin_([User.DEFAULT_USER, User.GHOST_USER, User.AI_USER])).count() + User.query() + .filter(User.username.notin_([User.DEFAULT_USER, User.GHOST_USER, User.AI_USER, User.SECURITY_USER])) + .count() ) assert response.json["recordsTotal"] == all_users assert response.json["recordsFiltered"] == 0 diff --git a/rhodecode/apps/admin/views/users.py b/rhodecode/apps/admin/views/users.py index 16c328f4..92a1cf8c 100644 --- a/rhodecode/apps/admin/views/users.py +++ b/rhodecode/apps/admin/views/users.py @@ -106,7 +106,7 @@ class AdminUsersView(BaseAppView, DataGridAppView): def user_actions(user_id, username): return _render("user_actions", user_id, username) - excluded_users = User.username.notin_([User.DEFAULT_USER, User.GHOST_USER, User.AI_USER]) + excluded_users = User.username.notin_([User.DEFAULT_USER, User.GHOST_USER, User.AI_USER, User.SECURITY_USER]) users_data_total_count = User.query().filter(excluded_users).count() users_data_total_inactive_count = User.query().filter(excluded_users).filter(User.active != true()).count() diff --git a/rhodecode/apps/home/views.py b/rhodecode/apps/home/views.py index c5a7b558..020d4ce4 100644 --- a/rhodecode/apps/home/views.py +++ b/rhodecode/apps/home/views.py @@ -244,7 +244,7 @@ class HomeView(BaseAppView, DataGridAppView): User.query() .order_by(func.length(User.username)) .order_by(User.username) - .filter(User.username.notin_([User.DEFAULT_USER, User.GHOST_USER, User.AI_USER])) + .filter(User.username.notin_([User.DEFAULT_USER, User.GHOST_USER, User.AI_USER, User.SECURITY_USER])) ) if name_contains: diff --git a/rhodecode/apps/repository/views/repo_pull_requests.py b/rhodecode/apps/repository/views/repo_pull_requests.py index e6374acf..8c71c007 100644 --- a/rhodecode/apps/repository/views/repo_pull_requests.py +++ b/rhodecode/apps/repository/views/repo_pull_requests.py @@ -450,6 +450,11 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView): if ai_pr_state: c.ai_code_review_state = ai_pr_state.get("review_state", None) + # Add security audit state + c.security_audit_state = None + if security_state := pull_request.security_audit_state: + c.security_audit_state = security_state + # inject latest version latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest) c.versions = versions + [latest_ver] @@ -963,6 +968,9 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView): close_branch_before_merging_key = "rhodecode_%s_close_branch_before_merging" % source_repo.repo_type c.repo_close_branch_before_merging = self._get_repo_setting(source_repo, close_branch_before_merging_key) + # Get security scan setting for pre-checking the checkbox + c.pr_security_scan_enabled = self._get_repo_setting(source_repo, "rhodecode_pr_security_scan_enabled", False) + return self._get_template_context(c) @LoginRequired() @@ -1274,6 +1282,35 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView): auth_user=self._rhodecode_user, settings=settings, ) + + # Handle security audit if requested (EE only) + run_security_audit = _form.get("run_security_audit", False) + if run_security_audit: + try: + from rc_ee.lib.celerylib import tasks as ee_tasks + from rhodecode.lib.celerylib import run_task + import datetime + + # Initialize security audit state + pull_request.security_audit_state = { + "status": "pending", + "started_at": datetime.datetime.utcnow().isoformat(), + "started_by": self._rhodecode_user.user_id, + } + + # Trigger async security audit task + run_task( + ee_tasks.audit_pull_request_diff, pull_request.pull_request_id, self._rhodecode_user.user_id + ) + + log.debug( + "Security audit triggered for PR #%s by user %s", + pull_request.pull_request_id, + self._rhodecode_user.username, + ) + except ImportError: + log.warning("EE security audit tasks not available, skipping") + Session().commit() h.flash(_("Successfully opened new pull request"), category="success") @@ -1550,6 +1587,34 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView): ) h.flash(msg, category="success") channelstream.pr_update_channelstream_push(self.request, c.pr_broadcast_channel, self._rhodecode_user, msg) + + # Re-run security audit if it was enabled for this PR + if pull_request.security_audit_state and pull_request.security_audit_state.get("status"): + try: + from rc_ee.lib.celerylib import tasks as ee_tasks + from rhodecode.lib.celerylib import run_task + import datetime + + # Reset state to pending for re-scan + pull_request.security_audit_state = { + "status": "pending", + "started_at": datetime.datetime.utcnow().isoformat(), + "started_by": self._rhodecode_user.user_id, + } + Session().commit() + + # Trigger async security audit task + run_task( + ee_tasks.audit_pull_request_diff, pull_request.pull_request_id, self._rhodecode_user.user_id + ) + + log.debug( + "Security audit re-triggered for PR #%s after update by user %s", + pull_request.pull_request_id, + self._rhodecode_user.username, + ) + except ImportError: + log.warning("EE security audit tasks not available, skipping re-scan") else: msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason] warning_reasons = [ diff --git a/rhodecode/apps/user_profile/views.py b/rhodecode/apps/user_profile/views.py index 66691a1d..6ca64c40 100644 --- a/rhodecode/apps/user_profile/views.py +++ b/rhodecode/apps/user_profile/views.py @@ -40,7 +40,7 @@ class UserProfileView(BaseAppView): username = self.request.matchdict.get("username") c.user = UserModel().get_by_username(username) - if not c.user or c.user.username in [User.DEFAULT_USER, User.GHOST_USER, User.AI_USER]: + if not c.user or c.user.username in [User.DEFAULT_USER, User.GHOST_USER, User.AI_USER, User.SECURITY_USER]: raise HTTPNotFound() return self._get_template_context(c) diff --git a/rhodecode/lib/db_manage.py b/rhodecode/lib/db_manage.py index 566d687b..98eaf386 100644 --- a/rhodecode/lib/db_manage.py +++ b/rhodecode/lib/db_manage.py @@ -632,6 +632,21 @@ class DbManage(object): Session().add(ai_user) Session().commit() + def create_security_user(self): + if not User.get_by_username(User.SECURITY_USER): + log.info("creating security user") + security_user = User() + security_user.name = "Security" + security_user.username = User.SECURITY_USER + security_user.lastname = "Scanner" + security_user.strict_creation_check = False + security_user.email = User.SECURITY_USER_EMAIL + security_user.active = False + security_user.description = "System user for security scanning" + security_user.is_new_user = False + Session().add(security_user) + Session().commit() + def create_default_user(self): log.info("creating default user") # create default user for handling default permissions. diff --git a/rhodecode/lib/dbmigrate/versions/122_version_5_10_0.py b/rhodecode/lib/dbmigrate/versions/122_version_5_10_0.py new file mode 100644 index 00000000..53de34e1 --- /dev/null +++ b/rhodecode/lib/dbmigrate/versions/122_version_5_10_0.py @@ -0,0 +1,48 @@ +import json +import logging + +from psycopg2.errorcodes import DUPLICATE_COLUMN +from sqlalchemy import * +from sqlalchemy.engine import reflection + +from alembic.migration import MigrationContext +from alembic.operations import Operations + +from rhodecode.lib.dbmigrate.versions import _reset_base +from rhodecode.lib.jsonalchemy import MutationObj, JsonType +from rhodecode.model import meta, init_model_encryption + + +def upgrade(migrate_engine): + """ + Upgrade operations go here. + Don't create your own engine; bind migrate_engine to your metadata + """ + _reset_base(migrate_engine) + + from rhodecode.lib.dbmigrate.schema import db_4_20_0_1 as db + + init_model_encryption(db) + + context = MigrationContext.configure(migrate_engine.connect()) + op = Operations(context) + inspector = inspect(migrate_engine) + + pr_tables = [db.PullRequest.__table__, db.PullRequestVersion.__table__] + for pr_table in pr_tables: + existing_columns = [col["name"] for col in inspector.get_columns(pr_table.name)] + new_column_name = "security_audit_state" + if new_column_name not in existing_columns: + with op.batch_alter_table(pr_table.name) as batch_op: + new_column = Column( + new_column_name, + MutationObj.as_mutable( + JsonType(dialect_map=dict(mysql=UnicodeText(16384))), + ), + default=dict, + ) + batch_op.add_column(new_column) + + +def downgrade(migrate_engine): + pass diff --git a/rhodecode/lib/dbmigrate/versions/123_version_5_10_0.py b/rhodecode/lib/dbmigrate/versions/123_version_5_10_0.py new file mode 100644 index 00000000..ef4924ed --- /dev/null +++ b/rhodecode/lib/dbmigrate/versions/123_version_5_10_0.py @@ -0,0 +1,37 @@ +from rhodecode.lib.dbmigrate.versions import _reset_base +from rhodecode.model import meta, init_model_encryption +from rhodecode.model.db import User + + +def upgrade(migrate_engine): + """ + Upgrade operations go here. + Don't create your own engine; bind migrate_engine to your metadata + """ + _reset_base(migrate_engine) + + from rhodecode.lib.dbmigrate.schema import db_4_20_0_1 as db + + init_model_encryption(db) + + UserModel: User = db.User + + username = "security_rhodecode" + if not User.get_by_username(username): + security_user = UserModel() + security_user.name = "Security" + security_user.username = username + security_user.lastname = "Scanner" + security_user.strict_creation_check = False + security_user.email = "security@rhodecode.org" + security_user.active = False + security_user.description = "System user for security scanning" + security_user.is_new_user = False + + session = meta.Session() + session.add(security_user) + session.commit() + + +def downgrade(migrate_engine): + pass diff --git a/rhodecode/lib/helpers.py b/rhodecode/lib/helpers.py index 288145d6..279e2ff0 100644 --- a/rhodecode/lib/helpers.py +++ b/rhodecode/lib/helpers.py @@ -1036,7 +1036,7 @@ def link_to_user(author, length=0, **kwargs): if length: display_person = shorter(display_person, length) - if user and user.username not in [user.DEFAULT_USER, user.GHOST_USER, user.AI_USER]: + if user and user.username not in [user.DEFAULT_USER, user.GHOST_USER, user.AI_USER, user.SECURITY_USER]: return link_to(escape(display_person), route_path("user_profile", username=user.username), **kwargs) else: return escape(display_person) diff --git a/rhodecode/lib/middleware/simplevcs.py b/rhodecode/lib/middleware/simplevcs.py index 0c83e557..cb5ff71e 100644 --- a/rhodecode/lib/middleware/simplevcs.py +++ b/rhodecode/lib/middleware/simplevcs.py @@ -484,7 +484,7 @@ class SimpleVCS(object): auth_user = user_obj.AuthUser() if ( user_obj - and user_obj.username not in [User.DEFAULT_USER, User.GHOST_USER, User.AI_USER] + and user_obj.username not in [User.DEFAULT_USER, User.GHOST_USER, User.AI_USER, User.SECURITY_USER] and user_obj.user_data.get("force_password_change") ): reason = "password change required" diff --git a/rhodecode/lib/rc_commands/setup_rc.py b/rhodecode/lib/rc_commands/setup_rc.py index 25cc3e94..c36b4aa2 100644 --- a/rhodecode/lib/rc_commands/setup_rc.py +++ b/rhodecode/lib/rc_commands/setup_rc.py @@ -98,6 +98,7 @@ def command( dbmanage.create_default_user() dbmanage.create_ghost_user() dbmanage.create_ai_user() + dbmanage.create_security_user() dbmanage.create_admin_and_prompt() dbmanage.create_permissions() dbmanage.populate_default_permissions() diff --git a/rhodecode/lib/utils.py b/rhodecode/lib/utils.py index f9180c2d..b421e26c 100644 --- a/rhodecode/lib/utils.py +++ b/rhodecode/lib/utils.py @@ -843,6 +843,7 @@ def create_test_database(test_path, config): dbmanage.create_default_user() dbmanage.create_ghost_user() dbmanage.create_ai_user() + dbmanage.create_security_user() dbmanage.create_test_admin_and_users() dbmanage.create_permissions() dbmanage.populate_default_permissions() @@ -878,7 +879,10 @@ def create_test_repositories(test_path, config): def password_changed(auth_user, session): # Never report password change in case of default user, anonymous user or system user. - if auth_user.username in [User.DEFAULT_USER, User.GHOST_USER, User.AI_USER] or auth_user.user_id is None: + if ( + auth_user.username in [User.DEFAULT_USER, User.GHOST_USER, User.AI_USER, User.SECURITY_USER] + or auth_user.user_id is None + ): return False password_hash = md5(safe_bytes(auth_user.password)) if auth_user.password else None diff --git a/rhodecode/model/db.py b/rhodecode/model/db.py index 1bb50f9d..32c61dcc 100644 --- a/rhodecode/model/db.py +++ b/rhodecode/model/db.py @@ -138,7 +138,7 @@ def display_user_sort(obj): of all other resources """ - if obj.username in [User.DEFAULT_USER, User.GHOST_USER, User.AI_USER]: + if obj.username in [User.DEFAULT_USER, User.GHOST_USER, User.AI_USER, User.SECURITY_USER]: return "#####" prefix = PERMISSION_TYPE_SORT.get(obj.permission.split(".")[-1], "") extra_sort_num = "1" # default @@ -622,8 +622,10 @@ class User(Base, BaseModel): DEFAULT_USER_EMAIL = "anonymous@rhodecode.org" GHOST_USER = "ghost" AI_USER = "ai_rhodecode" + SECURITY_USER = "security_rhodecode" GHOST_USER_EMAIL = "ghost@rhodecode.org" AI_USER_EMAIL = "ai@rhodecode.org" + SECURITY_USER_EMAIL = "security@rhodecode.org" DEFAULT_GRAVATAR_URL = "https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}" RECOVERY_CODES_COUNT = 10 @@ -732,7 +734,7 @@ class User(Base, BaseModel): last_activity_long_ago_or_no_activity_since_creation = (cls.last_activity < cutoff_date) | ( (cls.last_activity == null()) & (cls.created_on < cutoff_date) ) - exclude_system_users = ~cls.username.in_([cls.GHOST_USER, cls.AI_USER, cls.DEFAULT_USER]) + exclude_system_users = ~cls.username.in_([cls.GHOST_USER, cls.AI_USER, cls.DEFAULT_USER, cls.SECURITY_USER]) query = query.filter(exclude_system_users, last_activity_long_ago_or_no_activity_since_creation) @@ -1323,6 +1325,10 @@ class User(Base, BaseModel): def get_ai_user(cls, cache=False, refresh=False): return cls._get_system_user(username=cls.AI_USER, cache=cache, refresh=refresh) + @classmethod + def get_security_user(cls, cache=False, refresh=False): + return cls._get_system_user(username=cls.SECURITY_USER, cache=cache, refresh=refresh) + @classmethod def get_default_user(cls, cache=False, refresh=False): return cls._get_system_user(username=cls.DEFAULT_USER, cache=cache, refresh=refresh) @@ -3084,7 +3090,7 @@ class RepoGroup(Base, BaseModel): @classmethod def get_user_personal_repo_group(cls, user_id): user = User.get(user_id) - if user.username in [User.DEFAULT_USER, User.GHOST_USER, User.AI_USER]: + if user.username in [User.DEFAULT_USER, User.GHOST_USER, User.AI_USER, User.SECURITY_USER]: return None return cls.query().filter(cls.personal == true()).filter(cls.user == user).order_by(cls.group_id.asc()).first() @@ -4516,6 +4522,12 @@ class _PullRequestBase(BaseModel): default=dict, ) + security_audit_state = Column( + "security_audit_state", + MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=UnicodeText(DEFAULT_JSON_OBJ_SIZE)))), + default=dict, + ) + @property def reviewer_data_json(self): return str_json(self.reviewer_data) diff --git a/rhodecode/model/forms.py b/rhodecode/model/forms.py index 31b7b9dd..69915d53 100644 --- a/rhodecode/model/forms.py +++ b/rhodecode/model/forms.py @@ -443,6 +443,7 @@ class _BaseVcsSettingsForm(formencode.Schema): rhodecode_pr_merge_enabled = v.StringBoolean(if_missing=False) rhodecode_auto_merge_enabled = v.StringBoolean(if_missing=False) rhodecode_use_outdated_comments = v.StringBoolean(if_missing=False) + rhodecode_pr_security_scan_enabled = v.StringBoolean(if_missing=False) # hg extensions_largefiles = v.StringBoolean(if_missing=False) @@ -671,6 +672,7 @@ def PullRequestForm(localizer, repo_id): pullrequest_desc = v.UnicodeString(strip=True, required=False) description_renderer = v.UnicodeString(strip=True, required=False) close_branch_before_merging = v.StringBoolean(if_missing=False) + run_security_audit = v.StringBoolean(if_missing=False) return _PullRequestForm diff --git a/rhodecode/model/notification.py b/rhodecode/model/notification.py index fc34e769..4647ffad 100644 --- a/rhodecode/model/notification.py +++ b/rhodecode/model/notification.py @@ -315,6 +315,10 @@ class NotificationModel(BaseModel): _("%(user)s commented on pull request %(date_or_age)s"), _("%(user)s commented on pull request at %(date_or_age)s"), ], + EmailNotificationModel.TYPE_SECURITY_AUDIT: [ + _("%(user)s completed security audit %(date_or_age)s"), + _("%(user)s completed security audit at %(date_or_age)s"), + ], } templates = _map[notification.type_] @@ -380,6 +384,7 @@ class EmailNotificationModel(BaseModel): TYPE_EMAIL_EXCEPTION = "exception" TYPE_UPDATE_AVAILABLE = "update_available" TYPE_TEST = "test" + TYPE_SECURITY_AUDIT = "security_audit" email_types = { TYPE_MAIN: "rhodecode:templates/email_templates/main.mako", @@ -394,6 +399,7 @@ class EmailNotificationModel(BaseModel): TYPE_PULL_REQUEST: "rhodecode:templates/email_templates/pull_request_review.mako", TYPE_PULL_REQUEST_COMMENT: "rhodecode:templates/email_templates/pull_request_comment.mako", TYPE_PULL_REQUEST_UPDATE: "rhodecode:templates/email_templates/pull_request_update.mako", + TYPE_SECURITY_AUDIT: "rhodecode:templates/email_templates/security_audit.mako", } premailer_instance = premailer.Premailer( diff --git a/rhodecode/model/quota_model.py b/rhodecode/model/quota_model.py index 6d2218ef..cdbfd386 100644 --- a/rhodecode/model/quota_model.py +++ b/rhodecode/model/quota_model.py @@ -35,7 +35,12 @@ class UserQuotaModel: mb_allowance = self.max_disk_space_mb_allowance repo_count_allowance = self.max_repository_count_allowance - if self.user.admin or self.user.username in [User.AI_USER, User.DEFAULT_USER, User.GHOST_USER]: + if self.user.admin or self.user.username in [ + User.AI_USER, + User.DEFAULT_USER, + User.GHOST_USER, + User.SECURITY_USER, + ]: mb_allowance = self.UNLIMITED repo_count_allowance = self.UNLIMITED diff --git a/rhodecode/model/repo.py b/rhodecode/model/repo.py index 5fb7084c..8150c640 100644 --- a/rhodecode/model/repo.py +++ b/rhodecode/model/repo.py @@ -751,7 +751,7 @@ class RepoModel(BaseModel): if member_type == "user": member_name = User.get(member_id).username - if member_name in [User.GHOST_USER, User.AI_USER]: + if member_name in [User.GHOST_USER, User.AI_USER, User.SECURITY_USER]: raise ValueError("Modify user permissions for %s user is not allowed." % member_name) if member_name == User.DEFAULT_USER: diff --git a/rhodecode/model/repo_group.py b/rhodecode/model/repo_group.py index af6b3b5d..2133b1ae 100644 --- a/rhodecode/model/repo_group.py +++ b/rhodecode/model/repo_group.py @@ -359,7 +359,12 @@ class RepoGroupModel(BaseModel): elif isinstance(_obj, Repository): # private repos will not allow to change the default # permissions using recursive mode - if _obj.private and _user_obj.username in [User.DEFAULT_USER, User.GHOST_USER, User.AI_USER]: + if _obj.private and _user_obj.username in [ + User.DEFAULT_USER, + User.GHOST_USER, + User.AI_USER, + User.SECURITY_USER, + ]: log.debug("Skipping private repo %s for user %s", _obj, _user_obj) return @@ -381,7 +386,7 @@ class RepoGroupModel(BaseModel): elif isinstance(_obj, Repository): # private repos will not allow to change the default # permissions using recursive mode, also there's no revocation for default user, just update - if _user_obj.username in [User.DEFAULT_USER, User.GHOST_USER, User.AI_USER]: + if _user_obj.username in [User.DEFAULT_USER, User.GHOST_USER, User.AI_USER, User.SECURITY_USER]: log.debug("Skipping private repo %s for user %s", _obj, _user_obj) return RepoModel().revoke_user_permission(repo=_obj, user=_user_obj) @@ -426,7 +431,7 @@ class RepoGroupModel(BaseModel): member_obj = User.get(member_id) member_name = member_obj.username is_repo_group = isinstance(obj, RepoGroup) and obj == repo_group - if is_repo_group and member_name in [User.GHOST_USER, User.AI_USER]: + if is_repo_group and member_name in [User.GHOST_USER, User.AI_USER, User.SECURITY_USER]: raise ValueError("Modify user permissions for %s user is not allowed." % member_name) if is_repo_group and member_name == User.DEFAULT_USER: diff --git a/rhodecode/model/settings.py b/rhodecode/model/settings.py index 43aad79b..59863017 100644 --- a/rhodecode/model/settings.py +++ b/rhodecode/model/settings.py @@ -456,6 +456,7 @@ class VcsSettingsModel: "use_outdated_comments", "pr_merge_enabled", "auto_merge_enabled", + "pr_security_scan_enabled", "hg_use_rebase_for_merging", "hg_close_branch_before_merging", "hg_merge_strategy_selector", diff --git a/rhodecode/model/user.py b/rhodecode/model/user.py index d02fd051..f24b6f0b 100644 --- a/rhodecode/model/user.py +++ b/rhodecode/model/user.py @@ -153,7 +153,7 @@ class UserModel(BaseModel): qry = ( User.query() .filter(User.active == true()) - .filter(User.username.notin_([User.DEFAULT_USER, User.GHOST_USER, User.AI_USER])) + .filter(User.username.notin_([User.DEFAULT_USER, User.GHOST_USER, User.AI_USER, User.SECURITY_USER])) ) if cache: qry = qry.options(FromCache("sql_cache_short", "get_active_users")) @@ -189,7 +189,7 @@ class UserModel(BaseModel): def update_user(self, user, skip_attrs=None, **kwargs): user = self._get_user(user) - if user.username in [User.DEFAULT_USER, User.GHOST_USER, User.AI_USER]: + if user.username in [User.DEFAULT_USER, User.GHOST_USER, User.AI_USER, User.SECURITY_USER]: raise DefaultUserException( "You can't edit this user (`%(username)s`) since it's " "crucial for entire application" % {"username": user.username} @@ -373,7 +373,7 @@ class UserModel(BaseModel): edit = True # we're not allowed to edit default user or system user - if user.username in [User.DEFAULT_USER, User.GHOST_USER, User.AI_USER]: + if user.username in [User.DEFAULT_USER, User.GHOST_USER, User.AI_USER, User.SECURITY_USER]: raise DefaultUserException( "You can't edit this user (`%(username)s`) since it's " "crucial for entire application" % {"username": user.username} @@ -594,7 +594,7 @@ class UserModel(BaseModel): user = self._get_user(user) try: - if user.username in [User.DEFAULT_USER, User.GHOST_USER, User.AI_USER]: + if user.username in [User.DEFAULT_USER, User.GHOST_USER, User.AI_USER, User.SECURITY_USER]: raise DefaultUserException("You can't remove this user since it's crucial for entire application") if delete_reviewer: diff --git a/rhodecode/templates/base/vcs_settings.mako b/rhodecode/templates/base/vcs_settings.mako index 58d58645..8a6d671c 100644 --- a/rhodecode/templates/base/vcs_settings.mako +++ b/rhodecode/templates/base/vcs_settings.mako @@ -261,6 +261,24 @@
${_('When enabled, the target branch field will be automatically populated when creating pull requests. Disable this to prevent expensive diff calculations on repositories with many divergent branches.')}
+ + %if c.rhodecode_edition_id != 'EE': +
+ + +
+
+ ${_('This feature is available in RhodeCode EE edition only. Contact {sales_email} to obtain a trial license.').format(sales_email='sales@rhodecode.com')|n} +
+ %else: +
+ ${h.checkbox('rhodecode_pr_security_scan_enabled' + suffix, 'True', **kwargs)} + +
+
+ ${_('When enabled, the security scan checkbox will be pre-selected when creating pull requests. This scans the PR diff for exposed secrets and credentials.')} +
+ %endif % endif diff --git a/rhodecode/templates/email_templates/security_audit.mako b/rhodecode/templates/email_templates/security_audit.mako new file mode 100644 index 00000000..6c37cf22 --- /dev/null +++ b/rhodecode/templates/email_templates/security_audit.mako @@ -0,0 +1,108 @@ +<%inherit file="base.mako"/> +<%namespace name="base" file="base.mako"/> + +## EMAIL SUBJECT +<%def name="subject()" filter="n,trim,whitespace_filter"> +Security Audit Completed: ${repo_name} + + +## PLAINTEXT VERSION OF BODY +<%def name="body_plaintext()" filter="n,trim"> +Security Audit Results for ${repo_name} + +Files Scanned: ${scan_summary.get('files_scanned', 0)} +Secrets Found: ${scan_summary.get('secrets_found', 0)} + +% if scan_summary.get('secrets_found', 0) > 0: +FINDINGS: +% for finding in findings: +- ${finding['file']} (line ${finding['line']}): ${finding['type']} +% endfor +% else: +No exposed credentials or secrets were found. +% endif + +--- +${self.plaintext_footer()} + + +## header + + + + +
+
+
+ ${_('Security Scanner')} + ${_('completed audit for repository')} + ${repo_name} +
+
+
+ +
+ +## main body + + + + + + + + + + + + + + + + + + + + +
${_('Repository')}: + ${repo_name} +
${_('Files Scanned')}:${scan_summary.get('files_scanned', 0)}
${_('Secrets Found')}: + % if scan_summary.get('secrets_found', 0) > 0: + ${scan_summary.get('secrets_found', 0)} + % else: + 0 + % endif +
+ +% if scan_summary.get('secrets_found', 0) > 0: +
+ + + + + + + % for finding in findings: + + + + + + % endfor +
${_('File')}${_('Line')}${_('Type')}
${finding['file']}${finding['line']}${finding['type']}
+
+ +
+ ${_('Action Required')} +

+ ${_('Please review the findings above and remove or rotate any exposed credentials.')} +

+
+% else: +
+ ${_('All Clear')} +

+ ${_('No exposed credentials or secrets were found in this repository.')} +

+
+% endif diff --git a/rhodecode/templates/pullrequests/pullrequest.mako b/rhodecode/templates/pullrequests/pullrequest.mako index 081eec26..1ea1b7db 100644 --- a/rhodecode/templates/pullrequests/pullrequest.mako +++ b/rhodecode/templates/pullrequests/pullrequest.mako @@ -229,6 +229,24 @@ % endif + + % if c.rhodecode_edition_id == 'EE': +
+ ${h.checkbox('run_security_audit', checked=c.pr_security_scan_enabled, value=True)} + +
+ % else: +
+ ${h.checkbox('run_security_audit', checked=False, value=True, disabled=True, **{'class': 'disabled-checkbox'})} + +
+ % endif diff --git a/rhodecode/templates/pullrequests/pullrequest_show.mako b/rhodecode/templates/pullrequests/pullrequest_show.mako index 90c4d8a9..1509808b 100644 --- a/rhodecode/templates/pullrequests/pullrequest_show.mako +++ b/rhodecode/templates/pullrequests/pullrequest_show.mako @@ -731,6 +731,53 @@ % endif + % if c.rhodecode_edition_id == 'EE' and c.security_audit_state: + + % endif + ## TODOs