diff --git a/apps/accounts/automations/check_account/manager.py b/apps/accounts/automations/check_account/manager.py index f4e646dd6..c62b7e5d7 100644 --- a/apps/accounts/automations/check_account/manager.py +++ b/apps/accounts/automations/check_account/manager.py @@ -12,6 +12,7 @@ from accounts.models import Account, AccountRisk, RiskChoice from assets.automations.base.manager import BaseManager from common.const import ConfirmOrIgnore from common.decorators import bulk_create_decorator, bulk_update_decorator +from settings.models import LeakPasswords @bulk_create_decorator(AccountRisk) @@ -157,10 +158,8 @@ class CheckLeakHandler(BaseCheckHandler): if not account.secret: return False - sql = 'SELECT 1 FROM passwords WHERE password = ? LIMIT 1' - self.cursor.execute(sql, (account.secret,)) - leak = self.cursor.fetchone() is not None - return leak + is_exist = LeakPasswords.objects.using('sqlite').filter(password=account.secret).exists() + return is_exist def clean(self): self.cursor.close() diff --git a/apps/accounts/migrations/0007_alter_account_connectivity.py b/apps/accounts/migrations/0007_alter_account_connectivity.py new file mode 100644 index 000000000..8d7e775ae --- /dev/null +++ b/apps/accounts/migrations/0007_alter_account_connectivity.py @@ -0,0 +1,18 @@ +# Generated by Django 4.1.13 on 2025-05-06 10:23 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0006_alter_accountrisk_username_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='account', + name='connectivity', + field=models.CharField(choices=[('-', 'Unknown'), ('na', 'N/A'), ('ok', 'OK'), ('err', 'Error'), ('auth_err', 'Authentication error'), ('sudo_err', 'Sudo permission error'), ('password_err', 'Invalid password error'), ('openssh_key_err', 'OpenSSH key error'), ('ntlm_err', 'NTLM credentials rejected error'), ('create_dir_err', 'Create directory error')], default='-', max_length=16, verbose_name='Connectivity'), + ), + ] diff --git a/apps/assets/migrations/0019_alter_asset_connectivity.py b/apps/assets/migrations/0019_alter_asset_connectivity.py new file mode 100644 index 000000000..eb63c5b09 --- /dev/null +++ b/apps/assets/migrations/0019_alter_asset_connectivity.py @@ -0,0 +1,18 @@ +# Generated by Django 4.1.13 on 2025-05-06 10:23 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0018_rename_domain_zone'), + ] + + operations = [ + migrations.AlterField( + model_name='asset', + name='connectivity', + field=models.CharField(choices=[('-', 'Unknown'), ('na', 'N/A'), ('ok', 'OK'), ('err', 'Error'), ('auth_err', 'Authentication error'), ('sudo_err', 'Sudo permission error'), ('password_err', 'Invalid password error'), ('openssh_key_err', 'OpenSSH key error'), ('ntlm_err', 'NTLM credentials rejected error'), ('create_dir_err', 'Create directory error')], default='-', max_length=16, verbose_name='Connectivity'), + ), + ] diff --git a/apps/audits/signal_handlers/operate_log.py b/apps/audits/signal_handlers/operate_log.py index 7c53e6b6a..71c911edd 100644 --- a/apps/audits/signal_handlers/operate_log.py +++ b/apps/audits/signal_handlers/operate_log.py @@ -187,7 +187,7 @@ def on_django_start_set_operate_log_monitor_models(sender, **kwargs): 'PermedAsset', 'PermedAccount', 'MenuPermission', 'Permission', 'TicketSession', 'ApplyLoginTicket', 'ApplyCommandTicket', 'ApplyLoginAssetTicket', - 'FavoriteAsset', 'ChangeSecretRecord', 'AppProvider', 'Variable' + 'FavoriteAsset', 'ChangeSecretRecord', 'AppProvider', 'Variable', 'LeakPasswords' } include_models = {'UserSession'} for i, app in enumerate(apps.get_models(), 1): diff --git a/apps/authentication/mixins.py b/apps/authentication/mixins.py index c346ab1bb..62b816164 100644 --- a/apps/authentication/mixins.py +++ b/apps/authentication/mixins.py @@ -323,7 +323,7 @@ class AuthPostCheckMixin: def _check_passwd_is_too_simple(cls, user: User, password): if not user.is_auth_backend_model(): return - if user.check_passwd_too_simple(password): + if user.check_passwd_too_simple(password) or user.check_leak_password(password): message = _('Your password is too simple, please change it for security') url = cls.generate_reset_password_url_with_flash_msg(user, message=message) raise errors.PasswordTooSimple(url) diff --git a/apps/i18n/lina/zh.json b/apps/i18n/lina/zh.json index a2f5084c4..e141b9cab 100644 --- a/apps/i18n/lina/zh.json +++ b/apps/i18n/lina/zh.json @@ -1548,5 +1548,6 @@ "currentTime": "当前时间", "assetId": "资产 ID", "assetName": "资产名称", - "assetAddress": "资产地址" + "assetAddress": "资产地址", + "LeakPasswordList": "弱密码列表" } diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py index 34bda214c..655aa763b 100644 --- a/apps/jumpserver/conf.py +++ b/apps/jumpserver/conf.py @@ -705,7 +705,7 @@ class Config(dict): 'FILE_UPLOAD_SIZE_LIMIT_MB': 200, 'TICKET_APPLY_ASSET_SCOPE': 'all', - 'LEAK_PASSWORD_DB_PATH': os.path.join(PROJECT_DIR, 'data', 'leak_password.db'), + 'LEAK_PASSWORD_DB_PATH': os.path.join(PROJECT_DIR, 'data', 'leak_passwords.db'), # Ansible Receptor 'RECEPTOR_ENABLED': False, diff --git a/apps/settings/api/security.py b/apps/settings/api/security.py index 85de822f9..df10b627b 100644 --- a/apps/settings/api/security.py +++ b/apps/settings/api/security.py @@ -4,9 +4,11 @@ from django.conf import settings from django.core.cache import cache from rest_framework.generics import ListAPIView, CreateAPIView from rest_framework.views import Response +from rest_framework.viewsets import ModelViewSet from users.utils import LoginIpBlockUtil -from ..serializers import SecurityBlockIPSerializer +from ..models import LeakPasswords +from ..serializers import SecurityBlockIPSerializer, LeakPasswordPSerializer class BlockIPSecurityAPI(ListAPIView): @@ -56,3 +58,16 @@ class UnlockIPSecurityAPI(CreateAPIView): for ip in ips: LoginIpBlockUtil(ip).clean_block_if_need() return Response(status=200) + + +class LeakPasswordViewSet(ModelViewSet): + serializer_class = LeakPasswordPSerializer + model = LeakPasswords + rbac_perms = { + '*': 'settings.change_security' + } + queryset = LeakPasswords.objects.none() + search_fields = ['password'] + + def get_queryset(self): + return LeakPasswords.objects.using('sqlite').all() diff --git a/apps/settings/apps.py b/apps/settings/apps.py index fc87dc9f0..97c226e51 100644 --- a/apps/settings/apps.py +++ b/apps/settings/apps.py @@ -9,3 +9,9 @@ class SettingsConfig(AppConfig): def ready(self): from . import signal_handlers # noqa from . import tasks # noqa + from .models import init_sqlite_db, register_sqlite_connection + try: + init_sqlite_db() + register_sqlite_connection() + except Exception: + pass diff --git a/apps/settings/migrations/0002_leakpasswords.py b/apps/settings/migrations/0002_leakpasswords.py new file mode 100644 index 000000000..973a4bc68 --- /dev/null +++ b/apps/settings/migrations/0002_leakpasswords.py @@ -0,0 +1,24 @@ +# Generated by Django 4.1.13 on 2025-05-06 10:58 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('settings', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='LeakPasswords', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('password', models.CharField(max_length=1024, verbose_name='Password')), + ], + options={ + 'db_table': 'passwords', + 'managed': False, + }, + ), + ] diff --git a/apps/settings/models.py b/apps/settings/models.py index 32a6f5f4d..b7a4b72a6 100644 --- a/apps/settings/models.py +++ b/apps/settings/models.py @@ -1,10 +1,12 @@ import json +import os +import shutil from django.conf import settings from django.core.files.base import ContentFile from django.core.files.storage import default_storage from django.core.files.uploadedfile import InMemoryUploadedFile -from django.db import models +from django.db import models, connections from django.db.utils import ProgrammingError, OperationalError from django.utils.translation import gettext_lazy as _ from rest_framework.utils.encoders import JSONEncoder @@ -208,3 +210,38 @@ def get_chatai_data(): data['model'] = settings.DEEPSEEK_MODEL return data + + +def init_sqlite_db(): + db_path = settings.LEAK_PASSWORD_DB_PATH + if not os.path.isfile(db_path): + db_path = settings.LEAK_PASSWORD_DB_PATH + src = os.path.join( + settings.APPS_DIR, 'accounts', 'automations', + 'check_account', 'leak_passwords.db' + ) + shutil.copy(src, db_path) + logger.info(f'init sqlite db {db_path}') + return db_path + + +def register_sqlite_connection(): + connections.databases['sqlite'] = { + 'ENGINE': 'django.db.backends.sqlite3', + 'ATOMIC_REQUESTS': False, + 'NAME': settings.LEAK_PASSWORD_DB_PATH, + 'TIME_ZONE': None, + 'CONN_HEALTH_CHECKS': False, + 'CONN_MAX_AGE': 0, + 'OPTIONS': {}, + 'AUTOCOMMIT': True, + } + + +class LeakPasswords(models.Model): + id = models.AutoField(primary_key=True) + password = models.CharField(max_length=1024, verbose_name=_("Password")) + + class Meta: + db_table = 'passwords' + managed = False diff --git a/apps/settings/serializers/security.py b/apps/settings/serializers/security.py index d11ab53fd..0b83f0f99 100644 --- a/apps/settings/serializers/security.py +++ b/apps/settings/serializers/security.py @@ -7,9 +7,11 @@ __all__ = [ 'SecurityPasswordRuleSerializer', 'SecuritySessionSerializer', 'SecurityAuthSerializer', 'SecuritySettingSerializer', 'SecurityLoginLimitSerializer', 'SecurityBasicSerializer', - 'SecurityBlockIPSerializer' + 'SecurityBlockIPSerializer', 'LeakPasswordPSerializer' ] +from settings.models import LeakPasswords + class SecurityPasswordRuleSerializer(serializers.Serializer): SECURITY_PASSWORD_EXPIRATION_TIME = serializers.IntegerField( @@ -269,3 +271,20 @@ class SecuritySettingSerializer( class SecurityBlockIPSerializer(serializers.Serializer): id = serializers.UUIDField(required=False) ip = serializers.CharField(max_length=1024, required=False, allow_blank=True) + + +class LeakPasswordPSerializer(serializers.ModelSerializer): + + def create(self, validated_data): + return LeakPasswords.objects.using('sqlite').create(**validated_data) + + def update(self, instance, validated_data): + for attr, value in validated_data.items(): + setattr(instance, attr, value) + instance.save(using='sqlite') + return instance + + class Meta: + read_only_fields = ['id'] + fields = read_only_fields + ['password'] + model = LeakPasswords diff --git a/apps/settings/urls/api_urls.py b/apps/settings/urls/api_urls.py index 29e3c73e4..ce577cb23 100644 --- a/apps/settings/urls/api_urls.py +++ b/apps/settings/urls/api_urls.py @@ -8,6 +8,7 @@ from .. import api app_name = 'common' router = BulkRouter() router.register(r'chatai-prompts', api.ChatPromptViewSet, 'chatai-prompt') +router.register(r'leak-passwords', api.LeakPasswordViewSet, 'leak-passwords') urlpatterns = [ path('mail/testing/', api.MailTestingAPI.as_view(), name='mail-testing'), diff --git a/apps/users/models/user/_auth.py b/apps/users/models/user/_auth.py index e8d4b31ee..ea4c7aa6f 100644 --- a/apps/users/models/user/_auth.py +++ b/apps/users/models/user/_auth.py @@ -17,6 +17,7 @@ from common.utils import ( get_logger, lazyproperty, ) +from settings.models import LeakPasswords from users.signals import post_user_change_password logger = get_logger(__file__) @@ -298,3 +299,8 @@ class AuthMixin: return "" password = signer.unsign(secret) return password + + @staticmethod + def check_leak_password(password): + is_exist = LeakPasswords.objects.using('sqlite').filter(password=password).exists() + return is_exist diff --git a/apps/users/views/profile/reset.py b/apps/users/views/profile/reset.py index 561816f03..06320cd68 100644 --- a/apps/users/views/profile/reset.py +++ b/apps/users/views/profile/reset.py @@ -16,8 +16,8 @@ from authentication.utils import check_user_property_is_correct from common.const.choices import COUNTRY_CALLING_CODES from common.utils import FlashMessageUtil, get_object_or_none, random_string from common.utils.verify_code import SendAndVerifyCodeUtil -from users.serializers import SmsUserSerializer from users.notifications import ResetPasswordSuccessMsg +from users.serializers import SmsUserSerializer from ... import forms from ...models import User from ...utils import check_password_rules, get_password_check_rules @@ -220,6 +220,11 @@ class UserResetPasswordView(FormView): form.add_error('new_password', error) return self.form_invalid(form) + if user.check_leak_password(password): + error = _('Your password is too simple, please change it for security') + form.add_error('new_password', error) + return self.form_invalid(form) + user.reset_password(password) User.expired_reset_password_token(token)