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/apps/repository/views/repo_pull_requests.py b/rhodecode/apps/repository/views/repo_pull_requests.py index e6374acf..01d7c257 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] @@ -1274,6 +1279,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 +1584,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/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/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..8be09c5f 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() diff --git a/rhodecode/model/db.py b/rhodecode/model/db.py index 1bb50f9d..d9beb532 100644 --- a/rhodecode/model/db.py +++ b/rhodecode/model/db.py @@ -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 @@ -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) @@ -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..6c2ebe7c 100644 --- a/rhodecode/model/forms.py +++ b/rhodecode/model/forms.py @@ -671,6 +671,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..ef497484 100644 --- a/rhodecode/model/notification.py +++ b/rhodecode/model/notification.py @@ -380,6 +380,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 +395,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/templates/email_templates/security_audit.mako b/rhodecode/templates/email_templates/security_audit.mako new file mode 100644 index 00000000..a48c5c77 --- /dev/null +++ b/rhodecode/templates/email_templates/security_audit.mako @@ -0,0 +1,28 @@ +<%inherit file="base.mako"/> + +<%def name="subject()" filter="n,trim,whitespace_filter"> +Security Audit Completed: ${repo_name} +%def> + +<%def name="body_plaintext()" filter="n,trim"> +Security Audit Results for ${repo_name} + +${body} + +Files Scanned: ${scan_summary.get('files_scanned', 0)} +Secrets Found: ${scan_summary.get('secrets_found', 0)} + +--- +${self.plaintext_footer()} +%def> + +
+ Security Audit Results for ${repo_name}+${body}
+ Files Scanned: ${scan_summary.get('files_scanned', 0)} +Secrets Found: ${scan_summary.get('secrets_found', 0)} + |
+