Merge pull request !2949 from rhodecode-enterprise-ce security-audit-improvements
Changes from branch: Security audit improvements
This commit is contained in:
commit
01e582003c
29 changed files with 423 additions and 26 deletions
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
48
rhodecode/lib/dbmigrate/versions/122_version_5_10_0.py
Normal file
48
rhodecode/lib/dbmigrate/versions/122_version_5_10_0.py
Normal file
|
|
@ -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
|
||||
37
rhodecode/lib/dbmigrate/versions/123_version_5_10_0.py
Normal file
37
rhodecode/lib/dbmigrate/versions/123_version_5_10_0.py
Normal file
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -261,6 +261,24 @@
|
|||
<div class="label">
|
||||
<span class="help-block">${_('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.')}</span>
|
||||
</div>
|
||||
|
||||
%if c.rhodecode_edition_id != 'EE':
|
||||
<div class="checkbox">
|
||||
<input type="checkbox" disabled>
|
||||
<label>${_('Enable security scan for pull requests')}</label>
|
||||
</div>
|
||||
<div class="label">
|
||||
<span class="help-block">${_('This feature is available in RhodeCode EE edition only. Contact {sales_email} to obtain a trial license.').format(sales_email='<a href="mailto:sales@rhodecode.com">sales@rhodecode.com</a>')|n}</span>
|
||||
</div>
|
||||
%else:
|
||||
<div class="checkbox">
|
||||
${h.checkbox('rhodecode_pr_security_scan_enabled' + suffix, 'True', **kwargs)}
|
||||
<label for="rhodecode_pr_security_scan_enabled${suffix}">${_('Enable security scan for pull requests')}</label>
|
||||
</div>
|
||||
<div class="label">
|
||||
<span class="help-block">${_('When enabled, the security scan checkbox will be pre-selected when creating pull requests. This scans the PR diff for exposed secrets and credentials.')}</span>
|
||||
</div>
|
||||
%endif
|
||||
</div>
|
||||
</div>
|
||||
% endif
|
||||
|
|
|
|||
108
rhodecode/templates/email_templates/security_audit.mako
Normal file
108
rhodecode/templates/email_templates/security_audit.mako
Normal file
|
|
@ -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}
|
||||
</%def>
|
||||
|
||||
## 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()}
|
||||
</%def>
|
||||
|
||||
## header
|
||||
<table style="text-align:left;vertical-align:middle;width: 100%">
|
||||
<tr>
|
||||
<td style="width:100%;border-bottom:1px solid #dbd9da;">
|
||||
<div style="margin: 0; font-weight: bold">
|
||||
<div class="clear-both" style="margin-bottom: 4px">
|
||||
<span style="color:#7E7F7F">${_('Security Scanner')}</span>
|
||||
${_('completed audit for repository')}
|
||||
<a href="${repo_url}" style="${base.link_css()}">${repo_name}</a>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div class="clear-both"></div>
|
||||
|
||||
## main body
|
||||
<table style="text-align:left;vertical-align:middle;width: 100%">
|
||||
<tr>
|
||||
<td style="width: 130px"></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td style="padding-right:20px;"><strong>${_('Repository')}:</strong></td>
|
||||
<td>
|
||||
<a href="${repo_url}" style="${base.link_css()}">${repo_name}</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td style="padding-right:20px;"><strong>${_('Files Scanned')}:</strong></td>
|
||||
<td>${scan_summary.get('files_scanned', 0)}</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td style="padding-right:20px;"><strong>${_('Secrets Found')}:</strong></td>
|
||||
<td>
|
||||
% if scan_summary.get('secrets_found', 0) > 0:
|
||||
<span style="color: #e85e4d; font-weight: bold;">${scan_summary.get('secrets_found', 0)}</span>
|
||||
% else:
|
||||
<span style="color: #0ac878; font-weight: bold;">0</span>
|
||||
% endif
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
% if scan_summary.get('secrets_found', 0) > 0:
|
||||
<div style="margin-top: 20px;">
|
||||
<table style="text-align:left;vertical-align:middle;width: 100%; border-collapse: collapse;">
|
||||
<tr style="background-color: #f5f5f5;">
|
||||
<th style="padding: 8px; border: 1px solid #ddd; text-align: left;">${_('File')}</th>
|
||||
<th style="padding: 8px; border: 1px solid #ddd; text-align: left;">${_('Line')}</th>
|
||||
<th style="padding: 8px; border: 1px solid #ddd; text-align: left;">${_('Type')}</th>
|
||||
</tr>
|
||||
% for finding in findings:
|
||||
<tr>
|
||||
<td style="padding: 8px; border: 1px solid #ddd;"><code>${finding['file']}</code></td>
|
||||
<td style="padding: 8px; border: 1px solid #ddd;">${finding['line']}</td>
|
||||
<td style="padding: 8px; border: 1px solid #ddd;">${finding['type']}</td>
|
||||
</tr>
|
||||
% endfor
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 15px; padding: 10px; background-color: #fff3cd; border: 1px solid #ffc107; border-radius: 4px;">
|
||||
<strong style="color: #856404;">${_('Action Required')}</strong>
|
||||
<p style="margin: 5px 0 0 0; color: #856404;">
|
||||
${_('Please review the findings above and remove or rotate any exposed credentials.')}
|
||||
</p>
|
||||
</div>
|
||||
% else:
|
||||
<div style="margin-top: 15px; padding: 10px; background-color: #d4edda; border: 1px solid #28a745; border-radius: 4px;">
|
||||
<strong style="color: #155724;">${_('All Clear')}</strong>
|
||||
<p style="margin: 5px 0 0 0; color: #155724;">
|
||||
${_('No exposed credentials or secrets were found in this repository.')}
|
||||
</p>
|
||||
</div>
|
||||
% endif
|
||||
|
|
@ -229,6 +229,24 @@
|
|||
</label>
|
||||
</div>
|
||||
% endif
|
||||
|
||||
% if c.rhodecode_edition_id == 'EE':
|
||||
<div class="pull-request-settings">
|
||||
${h.checkbox('run_security_audit', checked=c.pr_security_scan_enabled, value=True)}
|
||||
<label for="run_security_audit">
|
||||
${h.literal(_('Run security audit on PR diff'))}
|
||||
<span class="tooltip" title="${_('Scan only the changes in this pull request for exposed secrets and credentials. Results will appear in the PR sidebar.')}">[?]</span>
|
||||
</label>
|
||||
</div>
|
||||
% else:
|
||||
<div class="pull-request-settings">
|
||||
${h.checkbox('run_security_audit', checked=False, value=True, disabled=True, **{'class': 'disabled-checkbox'})}
|
||||
<label for="run_security_audit" class="disabled-label">
|
||||
${h.literal(_('Run security audit on PR diff'))}
|
||||
<span class="ee-feature-label">[EE Only]</span>
|
||||
</label>
|
||||
</div>
|
||||
% endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -731,6 +731,53 @@
|
|||
</div>
|
||||
% endif
|
||||
|
||||
% if c.rhodecode_edition_id == 'EE' and c.security_audit_state:
|
||||
<div id="security-audit-section" class="sidebar-element clear-both">
|
||||
<div class="right-sidebar-expanded-state pr-details-title">
|
||||
<span class="sidebar-heading">
|
||||
<i class="icon-lock"></i>
|
||||
${_('Security Audit')}
|
||||
</span>
|
||||
</div>
|
||||
<div style="margin-top: 10px">
|
||||
% if c.security_audit_state.get('status') == 'pending':
|
||||
<span class="tag" style="background-color: #ffc854; color: #333;">
|
||||
<i class="icon-hourglass"></i>
|
||||
${_('Audit Pending')}
|
||||
</span>
|
||||
% elif c.security_audit_state.get('status') == 'scanning':
|
||||
<span class="tag" style="background-color: #5bc0de; color: #fff;">
|
||||
<i class="icon-refresh"></i>
|
||||
${_('Scanning...')}
|
||||
</span>
|
||||
% elif c.security_audit_state.get('status') == 'completed':
|
||||
<%
|
||||
count = c.security_audit_state.get('findings_count', 0)
|
||||
%>
|
||||
% if count > 0:
|
||||
<span class="tag" style="background-color: #ffc854; color: #333;">
|
||||
<i class="icon-attention"></i>
|
||||
${_('%s finding(s)') % count}
|
||||
</span>
|
||||
<p style="font-size: 11px; color: #666; margin-top: 5px;">
|
||||
${_('See inline comments for details')}
|
||||
</p>
|
||||
% else:
|
||||
<span class="tag" style="background-color: #0ac878; color: #fff;">
|
||||
<i class="icon-ok"></i>
|
||||
${_('No secrets found')}
|
||||
</span>
|
||||
% endif
|
||||
% elif c.security_audit_state.get('status') == 'failed':
|
||||
<span class="tag" style="background-color: #e85e4d; color: #fff;">
|
||||
<i class="icon-attention"></i>
|
||||
${_('Audit Failed')}
|
||||
</span>
|
||||
% endif
|
||||
</div>
|
||||
</div>
|
||||
% endif
|
||||
|
||||
## TODOs
|
||||
<div id="todosTable" class="sidebar-element clear-both">
|
||||
<div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="TODOs">
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ GENERAL_FORM_DATA = {
|
|||
"rhodecode_pr_merge_enabled": True,
|
||||
"rhodecode_auto_merge_enabled": True,
|
||||
"rhodecode_use_outdated_comments": True,
|
||||
"rhodecode_pr_security_scan_enabled": True,
|
||||
"rhodecode_hg_use_rebase_for_merging": True,
|
||||
"rhodecode_hg_close_branch_before_merging": True,
|
||||
"rhodecode_hg_merge_strategy_selector": True,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue